yadnyeshkolte commited on
Commit
448b355
·
1 Parent(s): 2891d39
backend/src/agents/orchestrator.ts CHANGED
@@ -125,18 +125,30 @@ export class Orchestrator {
125
 
126
  // Step 4: Quality Check
127
  onProgress("quality", "running", "Running quality validation...");
128
- const validationResult = await this.archestra.callTool("validate_readme", { readme });
129
- const validation = safeJsonParse(extractToolText(validationResult));
 
 
 
 
 
 
 
130
 
131
  let finalReadme = readme;
132
  const score = validation.score || 0;
133
  if (score < 80) {
134
  onProgress("quality", "running", `Score ${score}/100 — enhancing...`);
135
- const enhancedResult = await this.archestra.callTool("enhance_readme", {
136
- readme,
137
- suggestions: (validation.suggestions || []).join(", ")
138
- });
139
- finalReadme = extractToolText(enhancedResult);
 
 
 
 
 
140
  }
141
 
142
  onProgress("quality", "complete", `Quality Score: ${score}/100`);
@@ -252,8 +264,13 @@ export class Orchestrator {
252
 
253
  if (onProgress) onProgress("quality", "running", "Re-validating improved README...");
254
 
255
- const validationResult = await this.archestra.callTool("validate_readme", { readme: enhanced });
256
- const validation = safeJsonParse(extractToolText(validationResult));
 
 
 
 
 
257
 
258
  if (onProgress) onProgress("quality", "complete", `New Score: ${validation.score}/100`);
259
 
 
125
 
126
  // Step 4: Quality Check
127
  onProgress("quality", "running", "Running quality validation...");
128
+ let validation: any = { score: 70, categories: {}, suggestions: ["Quality scoring was unavailable — default score applied."] };
129
+ try {
130
+ const validationResult = await this.archestra.callTool("validate_readme", { readme });
131
+ const validationText = extractToolText(validationResult);
132
+ validation = safeJsonParse(validationText);
133
+ } catch (scoreError: any) {
134
+ console.warn("Quality scoring failed (non-fatal):", scoreError.message);
135
+ onProgress("quality", "running", "Scoring unavailable — proceeding with generated README...");
136
+ }
137
 
138
  let finalReadme = readme;
139
  const score = validation.score || 0;
140
  if (score < 80) {
141
  onProgress("quality", "running", `Score ${score}/100 — enhancing...`);
142
+ try {
143
+ const enhancedResult = await this.archestra.callTool("enhance_readme", {
144
+ readme,
145
+ suggestions: (validation.suggestions || []).join(", ")
146
+ });
147
+ finalReadme = extractToolText(enhancedResult);
148
+ } catch (enhanceError: any) {
149
+ console.warn("Enhancement failed (non-fatal):", enhanceError.message);
150
+ // Keep the original readme
151
+ }
152
  }
153
 
154
  onProgress("quality", "complete", `Quality Score: ${score}/100`);
 
264
 
265
  if (onProgress) onProgress("quality", "running", "Re-validating improved README...");
266
 
267
+ let validation: any = { score: 75, categories: {}, suggestions: ["Re-scoring was unavailable — default score applied."] };
268
+ try {
269
+ const validationResult = await this.archestra.callTool("validate_readme", { readme: enhanced });
270
+ validation = safeJsonParse(extractToolText(validationResult));
271
+ } catch (scoreError: any) {
272
+ console.warn("Re-validation scoring failed (non-fatal):", scoreError.message);
273
+ }
274
 
275
  if (onProgress) onProgress("quality", "complete", `New Score: ${validation.score}/100`);
276
 
backend/src/utils/json.ts CHANGED
@@ -1,43 +1,127 @@
1
  export function safeJsonParse(text: string): any {
 
 
 
 
 
2
  try {
3
- // Attempt standard parse
4
- return JSON.parse(text);
5
- } catch (e) {
6
- // Attempt to extract JSON from markdown code blocks
7
  const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/) || text.match(/```\s*([\s\S]*?)\s*```/);
8
  if (jsonMatch && jsonMatch[1]) {
9
  try {
10
  return JSON.parse(jsonMatch[1].trim());
11
- } catch (innerError) {
12
- throw new Error(`Failed to parse extracted JSON: ${innerError}`);
13
- }
14
  }
15
-
16
- // If no code blocks, try to find the first '{' or '[' and last '}' or ']'
17
- const firstBrace = text.indexOf('{');
18
- const lastBrace = text.lastIndexOf('}');
19
- const firstBracket = text.indexOf('[');
20
- const lastBracket = text.lastIndexOf(']');
21
 
22
  let start = -1;
23
  let end = -1;
24
-
25
  if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
26
- start = firstBrace;
27
- end = lastBrace;
28
  } else if (firstBracket !== -1) {
29
- start = firstBracket;
30
- end = lastBracket;
31
  }
32
 
33
  if (start !== -1 && end !== -1 && end > start) {
34
- try {
35
- return JSON.parse(text.substring(start, end + 1));
36
- } catch (innerError) {
37
- // Fall through to original error
38
- }
 
 
 
 
 
 
 
39
  }
40
 
41
- throw e;
42
  }
43
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  export function safeJsonParse(text: string): any {
2
+ // Pre-clean: strip markdown code fences the LLM may have wrapped around JSON
3
+ let cleaned = text.trim();
4
+ const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
5
+ if (fenceMatch) cleaned = fenceMatch[1].trim();
6
+
7
  try {
8
+ return JSON.parse(cleaned);
9
+ } catch (firstError) {
10
+ // ── Strategy 1: extract JSON from code blocks inside larger text ──
 
11
  const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/) || text.match(/```\s*([\s\S]*?)\s*```/);
12
  if (jsonMatch && jsonMatch[1]) {
13
  try {
14
  return JSON.parse(jsonMatch[1].trim());
15
+ } catch { /* fall through */ }
 
 
16
  }
17
+
18
+ // ── Strategy 2: find the outermost { } or [ … ] ──
19
+ const firstBrace = cleaned.indexOf('{');
20
+ const lastBrace = cleaned.lastIndexOf('}');
21
+ const firstBracket = cleaned.indexOf('[');
22
+ const lastBracket = cleaned.lastIndexOf(']');
23
 
24
  let start = -1;
25
  let end = -1;
 
26
  if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
27
+ start = firstBrace;
28
+ end = lastBrace;
29
  } else if (firstBracket !== -1) {
30
+ start = firstBracket;
31
+ end = lastBracket;
32
  }
33
 
34
  if (start !== -1 && end !== -1 && end > start) {
35
+ try {
36
+ return JSON.parse(cleaned.substring(start, end + 1));
37
+ } catch { /* fall through */ }
38
+ }
39
+
40
+ // ── Strategy 3: repair truncated JSON (e.g. unterminated strings) ──
41
+ // This happens when Gemini hits maxOutputTokens mid-response
42
+ if (start !== -1) {
43
+ let partial = cleaned.substring(start, end !== -1 && end > start ? end + 1 : undefined);
44
+ try {
45
+ return repairAndParseJSON(partial);
46
+ } catch { /* fall through */ }
47
  }
48
 
49
+ throw firstError;
50
  }
51
  }
52
+
53
+ /**
54
+ * Attempt to repair truncated JSON that was cut off mid-generation.
55
+ * Handles: unterminated strings, missing closing braces/brackets, trailing commas.
56
+ */
57
+ function repairAndParseJSON(raw: string): any {
58
+ let s = raw;
59
+
60
+ // Remove trailing incomplete key-value pairs (e.g. `, "key": "unterminated...`)
61
+ // Step 1: close any unterminated strings by counting quotes
62
+ let inString = false;
63
+ let escaped = false;
64
+ let lastGoodIndex = 0;
65
+
66
+ for (let i = 0; i < s.length; i++) {
67
+ const ch = s[i];
68
+ if (escaped) { escaped = false; continue; }
69
+ if (ch === '\\') { escaped = true; continue; }
70
+ if (ch === '"') {
71
+ inString = !inString;
72
+ if (!inString) lastGoodIndex = i; // end of a complete string
73
+ }
74
+ if (!inString) lastGoodIndex = i;
75
+ }
76
+
77
+ // If we ended inside a string, truncate to last good position and close the string
78
+ if (inString) {
79
+ s = s.substring(0, lastGoodIndex + 1);
80
+ // If the last good char was the opening quote of a value, remove the dangling key
81
+ // Otherwise close the string we were inside
82
+ }
83
+
84
+ // Remove trailing commas before we add closing braces
85
+ s = s.replace(/,\s*$/, '');
86
+
87
+ // Count open vs close braces and brackets
88
+ let braces = 0;
89
+ let brackets = 0;
90
+ inString = false;
91
+ escaped = false;
92
+ for (let i = 0; i < s.length; i++) {
93
+ const ch = s[i];
94
+ if (escaped) { escaped = false; continue; }
95
+ if (ch === '\\') { escaped = true; continue; }
96
+ if (ch === '"') { inString = !inString; continue; }
97
+ if (inString) continue;
98
+ if (ch === '{') braces++;
99
+ else if (ch === '}') braces--;
100
+ else if (ch === '[') brackets++;
101
+ else if (ch === ']') brackets--;
102
+ }
103
+
104
+ // Remove trailing incomplete entries (trailing commas, partial keys)
105
+ s = s.replace(/,\s*"[^"]*"?\s*:?\s*"?[^"]*$/, '');
106
+ s = s.replace(/,\s*$/, '');
107
+
108
+ // Re-count after cleanup
109
+ braces = 0; brackets = 0; inString = false; escaped = false;
110
+ for (let i = 0; i < s.length; i++) {
111
+ const ch = s[i];
112
+ if (escaped) { escaped = false; continue; }
113
+ if (ch === '\\') { escaped = true; continue; }
114
+ if (ch === '"') { inString = !inString; continue; }
115
+ if (inString) continue;
116
+ if (ch === '{') braces++;
117
+ else if (ch === '}') braces--;
118
+ else if (ch === '[') brackets++;
119
+ else if (ch === ']') brackets--;
120
+ }
121
+
122
+ // Close any remaining open brackets/braces
123
+ while (brackets > 0) { s += ']'; brackets--; }
124
+ while (braces > 0) { s += '}'; braces--; }
125
+
126
+ return JSON.parse(s);
127
+ }
mcp-servers/doc-generator/src/index.mts CHANGED
@@ -17,7 +17,7 @@ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
- async function callGemini(messages: any[], maxTokens: number = 4000) {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  // Convert OpenAI-style messages to Gemini format
@@ -124,7 +124,7 @@ function createServer() {
124
  // Style-specific system prompts and token limits
125
  const stylePrompts: Record<string, { prompt: string; tokens: number }> = {
126
  minimal: {
127
- tokens: 1500,
128
  prompt: `You are a concise technical writer. Generate a MINIMAL README.md — short, clean, and actionable.
129
 
130
  Include ONLY these sections:
@@ -141,7 +141,7 @@ Rules:
141
  - Maximum ~150 lines total`
142
  },
143
  standard: {
144
- tokens: 3000,
145
  prompt: `You are a world-class technical writer who creates beautiful, comprehensive README.md files. You write READMEs that developers LOVE — clear, professional, and visually appealing.
146
 
147
  Include these sections:
@@ -164,7 +164,7 @@ Rules:
164
  - Be thorough but concise`
165
  },
166
  detailed: {
167
- tokens: 4000,
168
  prompt: `You are a world-class technical writer who creates beautiful, comprehensive README.md files for open-source projects. You write READMEs that developers LOVE — clear, professional, and visually appealing.
169
 
170
  Your README MUST include ALL of the following sections (skip only if truly irrelevant):
 
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
+ async function callGemini(messages: any[], maxTokens: number = 8192) {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  // Convert OpenAI-style messages to Gemini format
 
124
  // Style-specific system prompts and token limits
125
  const stylePrompts: Record<string, { prompt: string; tokens: number }> = {
126
  minimal: {
127
+ tokens: 4096,
128
  prompt: `You are a concise technical writer. Generate a MINIMAL README.md — short, clean, and actionable.
129
 
130
  Include ONLY these sections:
 
141
  - Maximum ~150 lines total`
142
  },
143
  standard: {
144
+ tokens: 8192,
145
  prompt: `You are a world-class technical writer who creates beautiful, comprehensive README.md files. You write READMEs that developers LOVE — clear, professional, and visually appealing.
146
 
147
  Include these sections:
 
164
  - Be thorough but concise`
165
  },
166
  detailed: {
167
+ tokens: 16384,
168
  prompt: `You are a world-class technical writer who creates beautiful, comprehensive README.md files for open-source projects. You write READMEs that developers LOVE — clear, professional, and visually appealing.
169
 
170
  Your README MUST include ALL of the following sections (skip only if truly irrelevant):
mcp-servers/readme-improver/src/index.mts CHANGED
@@ -17,7 +17,7 @@ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
- async function callGemini(messages: any[], maxTokens = 4000): Promise<string> {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  const systemInstruction = messages.find((m: any) => m.role === 'system')?.content || '';
@@ -139,7 +139,7 @@ ${readme.substring(0, 15000)}`;
139
  const improved = await callGemini([
140
  { role: "system", content: "You are an expert technical writer who improves open-source documentation. Return ONLY the improved README in raw Markdown. Never wrap in code blocks." },
141
  { role: "user", content: prompt }
142
- ], 4000);
143
  return {
144
  content: [{ type: "text", text: improved }],
145
  };
 
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
+ async function callGemini(messages: any[], maxTokens = 8192): Promise<string> {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  const systemInstruction = messages.find((m: any) => m.role === 'system')?.content || '';
 
139
  const improved = await callGemini([
140
  { role: "system", content: "You are an expert technical writer who improves open-source documentation. Return ONLY the improved README in raw Markdown. Never wrap in code blocks." },
141
  { role: "user", content: prompt }
142
+ ], 8192);
143
  return {
144
  content: [{ type: "text", text: improved }],
145
  };
mcp-servers/readme-scorer/src/index.mts CHANGED
@@ -17,7 +17,7 @@ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
- async function callGemini(messages: any[], maxTokens = 1000): Promise<string> {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  const systemInstruction = messages.find((m: any) => m.role === 'system')?.content || '';
@@ -34,16 +34,20 @@ async function callGemini(messages: any[], maxTokens = 1000): Promise<string> {
34
  try {
35
  const controller = new AbortController();
36
  const timeout = setTimeout(() => controller.abort(), 90_000);
 
 
 
 
 
 
 
37
  const response = await fetch(GEMINI_URL, {
38
  method: "POST",
39
  headers: { "Content-Type": "application/json" },
40
  body: JSON.stringify({
41
  ...(systemInstruction ? { systemInstruction: { parts: [{ text: systemInstruction }] } } : {}),
42
  contents,
43
- generationConfig: {
44
- temperature: 0.3,
45
- maxOutputTokens: maxTokens,
46
- },
47
  }),
48
  signal: controller.signal,
49
  });
@@ -143,7 +147,7 @@ ${readme.substring(0, 12000)}`;
143
  const validation = await callGemini([
144
  { role: "system", content: "You are a documentation QA specialist who scores README files. Return ONLY valid JSON, no markdown wrapping, no backticks." },
145
  { role: "user", content: prompt }
146
- ]);
147
  return {
148
  content: [{ type: "text", text: validation }],
149
  };
 
17
  const GEMINI_MODEL = "gemini-2.5-flash";
18
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
19
 
20
+ async function callGemini(messages: any[], maxTokens = 2048, responseFormat?: string): Promise<string> {
21
  if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set");
22
 
23
  const systemInstruction = messages.find((m: any) => m.role === 'system')?.content || '';
 
34
  try {
35
  const controller = new AbortController();
36
  const timeout = setTimeout(() => controller.abort(), 90_000);
37
+ const generationConfig: any = {
38
+ temperature: 0.3,
39
+ maxOutputTokens: maxTokens,
40
+ };
41
+ if (responseFormat) {
42
+ generationConfig.responseMimeType = responseFormat;
43
+ }
44
  const response = await fetch(GEMINI_URL, {
45
  method: "POST",
46
  headers: { "Content-Type": "application/json" },
47
  body: JSON.stringify({
48
  ...(systemInstruction ? { systemInstruction: { parts: [{ text: systemInstruction }] } } : {}),
49
  contents,
50
+ generationConfig,
 
 
 
51
  }),
52
  signal: controller.signal,
53
  });
 
147
  const validation = await callGemini([
148
  { role: "system", content: "You are a documentation QA specialist who scores README files. Return ONLY valid JSON, no markdown wrapping, no backticks." },
149
  { role: "user", content: prompt }
150
+ ], 2048, "application/json");
151
  return {
152
  content: [{ type: "text", text: validation }],
153
  };