Skip to content

Commit 84eb24e

Browse files
Workclaude
authored andcommitted
Replace Groq with Gemini Flash everywhere + fix agent error propagation
- Add lib/ai/gemini.ts: Gemini 2.0 Flash via OpenAI-compatible API (json, text, tool loop) - Wire Gemini into all agents (TRIAGE, INVESTIGATOR, NETWORK_REASONER, DEVILS_ADVOCATE, SCRIBE) and so-what route; Groq kept as fallback when GOOGLE_AI_API_KEY is absent - Fix failed_generation 400 errors in groq.ts: return null parsed instead of throwing when all retries exhaust - Fix graph.v2.ts: move createRunningStep inside try/catch; wrap syncClaimsFromAgentOutputs with .catch() to prevent graph crashes - Fix orchestrator.ts: isolate persistRuntimeResultV2/handlePostAgentRun/ensureDefaultMonitors so DB errors don't mark agent runs as ERROR - Reduce INVESTIGATOR tools from 8 to 6 to lower Groq failed_generation rate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2312dcd commit 84eb24e

7 files changed

Lines changed: 292 additions & 55 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ UPSTOX_ACCESS_TOKEN=
99
DATABASE_URL="postgresql://localhost:5432/smallens"
1010
REDIS_URL="redis://localhost:6379"
1111
GROQ_API_KEY=
12+
GOOGLE_AI_API_KEY=
13+
GEMINI_MODEL=gemini-2.0-flash
1214
GROQ_MODEL="llama-3.1-8b-instant"
1315
GROQ_AGENT_DELAY_MS=3000
1416
AGENT_QUEUE_CONCURRENCY=1

app/api/company/[ticker]/so-what/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from "next/server";
22
import { auth } from "@/lib/auth/session";
33
import { prisma } from "@/lib/db";
4+
import { runGeminiText, isGeminiAvailable } from "@/lib/ai/gemini";
45
import { runGroqText, GROQ_MODELS } from "@/lib/ai/groq";
56
import Redis from "ioredis";
67

@@ -61,11 +62,10 @@ Metric: ${metric}
6162
Recent Values (chronological): ${JSON.stringify(values)}
6263
`;
6364

64-
const response = await runGroqText({
65-
system: "You are a senior equity analyst writing for serious Indian retail investors. Write 2-3 sentences maximum. Be direct. Reference specific numbers. If trend is bad, say so. Never use filler phrases. End with what the investor should watch next. Format your response using markdown bolding (**bold**) for key numbers, percentages, and important trends.",
66-
user: prompt,
67-
model: GROQ_MODELS.quick
68-
});
65+
const soWhatSystem = "You are a senior equity analyst writing for serious Indian retail investors. Write 2-3 sentences maximum. Be direct. Reference specific numbers. If trend is bad, say so. Never use filler phrases. End with what the investor should watch next. Format your response using markdown bolding (**bold**) for key numbers, percentages, and important trends.";
66+
const response = isGeminiAvailable()
67+
? await runGeminiText({ system: soWhatSystem, user: prompt })
68+
: await runGroqText({ system: soWhatSystem, user: prompt, model: GROQ_MODELS.quick });
6969

7070
const resultText = response.text || "Analysis unavailable.";
7171

lib/agents/orchestrator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,16 @@ export async function processAgentRun(agentRunId: string) {
5454
}
5555
});
5656

57+
let state: Awaited<ReturnType<typeof runResearchRuntimeV2>> | null = null;
5758
try {
58-
const state = await runResearchRuntimeV2({
59+
state = await runResearchRuntimeV2({
5960
runId: run.id,
6061
companyId: run.companyId,
6162
ticker: run.company.ticker,
6263
companyName: run.company.name,
6364
mission: run.mission,
6465
eventType: inferEventType(run.mission)
6566
});
66-
67-
await persistRuntimeResultV2(state);
68-
await handlePostAgentRun(state);
69-
await ensureDefaultMonitors(state.ticker, state.runId);
7067
} catch (error) {
7168
await prisma.agentRun.update({
7269
where: { id: run.id },
@@ -78,4 +75,15 @@ export async function processAgentRun(agentRunId: string) {
7875
});
7976
throw error;
8077
}
78+
79+
// Post-graph persistence — errors here should not mark the run as ERROR
80+
await persistRuntimeResultV2(state).catch((err) =>
81+
console.error(`[ORCHESTRATOR] persistRuntimeResultV2 failed for ${run.id}:`, err instanceof Error ? err.message : err)
82+
);
83+
await handlePostAgentRun(state).catch((err) =>
84+
console.error(`[ORCHESTRATOR] handlePostAgentRun failed for ${run.id}:`, err instanceof Error ? err.message : err)
85+
);
86+
await ensureDefaultMonitors(state.ticker, state.runId).catch((err) =>
87+
console.error(`[ORCHESTRATOR] ensureDefaultMonitors failed for ${run.id}:`, err instanceof Error ? err.message : err)
88+
);
8189
}

lib/agents/runtime/agents.v2.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from "zod";
2+
import { runGeminiJson, runGeminiToolLoopJson, runGeminiText, isGeminiAvailable, GEMINI_MODEL } from "@/lib/ai/gemini";
23
import { GROQ_MODELS, runGroqJson, runGroqToolLoopJson } from "@/lib/ai/groq";
34
import {
45
agentOutputSchema,
@@ -126,14 +127,7 @@ function createReActAgent(config: {
126127
null,
127128
2
128129
).slice(0, 8000);
129-
try {
130-
const response = await withAgentTimeout(`${config.id}`, 180000, () =>
131-
runGroqToolLoopJson<AgentOutput>({
132-
model: config.model ?? GROQ_MODELS.investigator,
133-
tools: getRuntimeToolDefinitions(config.tools),
134-
executeTool: executeRuntimeTool,
135-
maxToolCalls: Math.min(config.maxIterations, 6),
136-
system: `${config.system}
130+
const agentSystem = `${config.system}
137131
Allowed tools (use exact names only): ${config.tools.join(", ")}.
138132
Return strict JSON matching exactly this contract:
139133
{
@@ -147,9 +141,26 @@ Return strict JSON matching exactly this contract:
147141
"claims": ["string"],
148142
"flags": [{"type": "string", "severity": "HIGH", "evidenceId": "string", "confidence": 0.9}],
149143
"metrics": {}
150-
}`,
151-
user: slimUser
152-
})
144+
}`;
145+
try {
146+
const response = await withAgentTimeout(`${config.id}`, 180000, () =>
147+
isGeminiAvailable()
148+
? runGeminiToolLoopJson<AgentOutput>({
149+
model: GEMINI_MODEL,
150+
tools: getRuntimeToolDefinitions(config.tools),
151+
executeTool: executeRuntimeTool,
152+
maxToolCalls: Math.min(config.maxIterations, 6),
153+
system: agentSystem,
154+
user: slimUser
155+
})
156+
: runGroqToolLoopJson<AgentOutput>({
157+
model: config.model ?? GROQ_MODELS.investigator,
158+
tools: getRuntimeToolDefinitions(config.tools),
159+
executeTool: executeRuntimeTool,
160+
maxToolCalls: Math.min(config.maxIterations, 6),
161+
system: agentSystem,
162+
user: slimUser
163+
})
153164
);
154165

155166
const parsed = agent.outputSchema.safeParse(response.parsed);
@@ -190,10 +201,7 @@ export const triageAgent: RuntimeAgent<RuntimeAgentInput, TriageOutput> = {
190201
maxIterations: 1,
191202
async run(ctx) {
192203
const input = { ticker: ctx.state.ticker, eventType: ctx.state.eventType, mission: ctx.state.mission };
193-
const response = await withAgentTimeout("TRIAGE", 30000, () =>
194-
runGroqJson<TriageOutput>({
195-
model: GROQ_MODELS.triage,
196-
system: `You are TRIAGE for an Indian smallcap forensics platform.
204+
const triageSystem = `You are TRIAGE for an Indian smallcap forensics platform.
197205
198206
Decide investigation depth and pick agents:
199207
- SEBI_ACTION / AUDITOR_RESIGNATION / PROMOTER_PLEDGE_CHANGE → requiredRuntimeDepth: "DEEP", executionPlan: ["INVESTIGATOR", "NETWORK_REASONER"]
@@ -206,9 +214,11 @@ Set signalSeverity based on eventType:
206214
- MANUAL_MISSION = "MEDIUM"
207215
208216
confidenceFloor: always 0.55 (lower threshold allows agents to proceed with available data).
209-
Return strict JSON.`,
210-
user: JSON.stringify(input)
211-
})
217+
Return strict JSON.`;
218+
const response = await withAgentTimeout("TRIAGE", 30000, () =>
219+
isGeminiAvailable()
220+
? runGeminiJson<TriageOutput>({ system: triageSystem, user: JSON.stringify(input) })
221+
: runGroqJson<TriageOutput>({ model: GROQ_MODELS.triage, system: triageSystem, user: JSON.stringify(input) })
212222
);
213223

214224
const parsed = this.outputSchema.safeParse(response.parsed);
@@ -242,10 +252,8 @@ export const runtimeAgentsV2 = {
242252
tools: [
243253
"fetchQuarterRange",
244254
"fetchPromoterHoldingHistory",
245-
"fetchPledgeHistory",
246255
"fetchAuditorChanges",
247256
"fetchSEBIActions",
248-
"fetchNewsTimeline",
249257
"detectTrend",
250258
"queryMemory",
251259
],
@@ -332,10 +340,7 @@ export const devilsAdvocateAgent: RuntimeAgent<RuntimeAgentInput, DevilsAdvocate
332340
])
333341
);
334342
const input = { ticker: ctx.state.ticker, outputs: slimOutputs };
335-
const response = await withAgentTimeout("DEVILS_ADVOCATE", 45000, () =>
336-
runGroqJson<DevilsAdvocateOutput>({
337-
model: GROQ_MODELS.devilsAdvocate,
338-
system: `You are DEVILS_ADVOCATE for an Indian smallcap forensics platform.
343+
const daSystem = `You are DEVILS_ADVOCATE for an Indian smallcap forensics platform.
339344
340345
Your job:
341346
1. Review all agent outputs for internal contradictions or unsupported claims
@@ -350,9 +355,11 @@ IMPORTANT RULES:
350355
- overallConfidence = weighted average of agent confidences, minimum 0.45
351356
- If one agent found a CRITICAL flag, overall confidence should be >= 0.65
352357
353-
Return strict JSON.`,
354-
user: JSON.stringify(input).slice(0, 7000)
355-
})
358+
Return strict JSON.`;
359+
const response = await withAgentTimeout("DEVILS_ADVOCATE", 45000, () =>
360+
isGeminiAvailable()
361+
? runGeminiJson<DevilsAdvocateOutput>({ system: daSystem, user: JSON.stringify(input).slice(0, 7000) })
362+
: runGroqJson<DevilsAdvocateOutput>({ model: GROQ_MODELS.devilsAdvocate, system: daSystem, user: JSON.stringify(input).slice(0, 7000) })
356363
);
357364
const parsed = this.outputSchema.safeParse(response.parsed);
358365
const tokensUsed = response.usage?.total_tokens ?? 0;
@@ -398,10 +405,7 @@ export const scribeAgent: RuntimeAgent<RuntimeAgentInput, ScribeOutput> = {
398405
])
399406
);
400407
const input = { ticker: ctx.state.ticker, companyName: ctx.state.companyName, outputs: slimOutputs };
401-
const response = await withAgentTimeout("SCRIBE", 45000, () =>
402-
runGroqJson<ScribeOutput>({
403-
model: GROQ_MODELS.scribe,
404-
system: `You are a senior equity analyst writing for serious Indian retail investors.
408+
const scribeSystem = `You are a senior equity analyst writing for serious Indian retail investors.
405409
406410
Write a definitive investment memo based on all prior agent findings.
407411
@@ -420,9 +424,11 @@ REQUIRED CONTENT:
420424
- unresolvedRisks: items that need monitoring
421425
422426
AVOID: Generic phrases like "company shows mixed signals" or "further analysis needed". Be specific and cite numbers.
423-
Return strict JSON matching the required schema.`,
424-
user: JSON.stringify(input).slice(0, 7000)
425-
})
427+
Return strict JSON matching the required schema.`;
428+
const response = await withAgentTimeout("SCRIBE", 45000, () =>
429+
isGeminiAvailable()
430+
? runGeminiJson<ScribeOutput>({ system: scribeSystem, user: JSON.stringify(input).slice(0, 7000) })
431+
: runGroqJson<ScribeOutput>({ model: GROQ_MODELS.scribe, system: scribeSystem, user: JSON.stringify(input).slice(0, 7000) })
426432
);
427433
const parsed = this.outputSchema.safeParse(response.parsed);
428434
const tokensUsed = response.usage?.total_tokens ?? 0;

lib/agents/runtime/graph.v2.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,9 @@ async function parallelAgentMeshNode(state: RuntimeState) {
281281
for (const agentId of planned) {
282282
await pauseBetweenAgentCalls();
283283
const agentStart = Date.now();
284-
const step = await createRunningStep(state, agentId);
284+
let step: Awaited<ReturnType<typeof createRunningStep>> | null = null;
285285
try {
286+
step = await createRunningStep(state, agentId);
286287
const agent =
287288
agentId === "NETWORK_REASONER"
288289
? runtimeAgentsV2.NETWORK_REASONER
@@ -306,7 +307,7 @@ async function parallelAgentMeshNode(state: RuntimeState) {
306307
totalTools += output.toolCallCount ?? 0;
307308
results.push({ agentId, output, latencyMs: Date.now() - agentStart });
308309
} catch (error) {
309-
await failStep(step.id, error);
310+
if (step) await failStep(step.id, error).catch(() => {});
310311
const errMsg = error instanceof Error ? error.message : `${agentId} failed`;
311312
console.error(`[PARALLEL_AGENT_MESH] ${agentId} failed, using fallback:`, errMsg);
312313
const fallback = {
@@ -330,7 +331,7 @@ async function parallelAgentMeshNode(state: RuntimeState) {
330331
state.ticker,
331332
state.runId,
332333
investigatorOutputs
333-
);
334+
).catch((err) => console.error("[PARALLEL_AGENT_MESH] syncClaimsFromAgentOutputs failed:", err instanceof Error ? err.message : err));
334335
}
335336

336337
return {
@@ -394,12 +395,13 @@ async function rerunInvestigatorsNode(state: RuntimeState) {
394395
await pauseBetweenAgentCalls();
395396
const agentStart = Date.now();
396397
const reason = requests.find((request) => request.agent === agentId)?.reason ?? null;
397-
const step = await createRunningStep(
398-
state,
399-
agentId,
400-
`Rerun requested: ${reason ?? "confidence reinforcement"}`
401-
);
398+
let step: Awaited<ReturnType<typeof createRunningStep>> | null = null;
402399
try {
400+
step = await createRunningStep(
401+
state,
402+
agentId,
403+
`Rerun requested: ${reason ?? "confidence reinforcement"}`
404+
);
403405
const agent =
404406
agentId === "NETWORK_REASONER"
405407
? runtimeAgentsV2.NETWORK_REASONER
@@ -423,7 +425,7 @@ async function rerunInvestigatorsNode(state: RuntimeState) {
423425
totalTools += output.toolCallCount ?? 0;
424426
results.push({ agentId, output, latencyMs: Date.now() - agentStart });
425427
} catch (error) {
426-
await failStep(step.id, error);
428+
if (step) await failStep(step.id, error).catch(() => {});
427429
const errMsg = error instanceof Error ? error.message : `${agentId} rerun failed`;
428430
console.error(`[RERUN_INVESTIGATORS] ${agentId} failed, using fallback:`, errMsg);
429431
const fallback = {
@@ -447,7 +449,7 @@ async function rerunInvestigatorsNode(state: RuntimeState) {
447449
state.ticker,
448450
state.runId,
449451
investigatorOutputs
450-
);
452+
).catch((err) => console.error("[RERUN_INVESTIGATORS] syncClaimsFromAgentOutputs failed:", err instanceof Error ? err.message : err));
451453
}
452454

453455
return {

0 commit comments

Comments
 (0)