agent-starter-pack 0.5.3__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/METADATA +1 -1
- {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/RECORD +59 -33
- agents/adk_base/notebooks/adk_app_testing.ipynb +1 -1
- agents/adk_gemini_fullstack/README.md +148 -0
- agents/adk_gemini_fullstack/app/agent.py +363 -0
- src/frontends/streamlit_adk/frontend/style/app_markdown.py → agents/adk_gemini_fullstack/app/config.py +19 -23
- agents/adk_gemini_fullstack/notebooks/adk_app_testing.ipynb +353 -0
- agents/adk_gemini_fullstack/notebooks/evaluating_adk_agent.ipynb +1528 -0
- agents/adk_gemini_fullstack/template/.templateconfig.yaml +37 -0
- agents/adk_gemini_fullstack/tests/integration/test_agent.py +58 -0
- agents/agentic_rag/notebooks/adk_app_testing.ipynb +1 -1
- src/base_template/Makefile +21 -2
- src/base_template/README.md +8 -3
- src/base_template/pyproject.toml +1 -4
- src/cli/commands/create.py +17 -10
- src/cli/utils/template.py +13 -10
- src/deployment_targets/cloud_run/app/server.py +7 -1
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +1 -1
- src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +321 -0
- src/frontends/adk_gemini_fullstack/frontend/components.json +21 -0
- src/frontends/adk_gemini_fullstack/frontend/eslint.config.js +28 -0
- src/frontends/adk_gemini_fullstack/frontend/index.html +12 -0
- src/frontends/adk_gemini_fullstack/frontend/package-lock.json +5829 -0
- src/frontends/adk_gemini_fullstack/frontend/package.json +46 -0
- src/frontends/adk_gemini_fullstack/frontend/public/vite.svg +1 -0
- src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +565 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ActivityTimeline.tsx +244 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ChatMessagesView.tsx +419 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/InputForm.tsx +60 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/WelcomeScreen.tsx +56 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/badge.tsx +46 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/button.tsx +59 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/card.tsx +92 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/input.tsx +21 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/scroll-area.tsx +56 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/select.tsx +183 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/tabs.tsx +64 -0
- src/frontends/adk_gemini_fullstack/frontend/src/components/ui/textarea.tsx +18 -0
- src/frontends/adk_gemini_fullstack/frontend/src/global.css +154 -0
- src/frontends/adk_gemini_fullstack/frontend/src/main.tsx +13 -0
- src/frontends/adk_gemini_fullstack/frontend/src/utils.ts +7 -0
- src/frontends/adk_gemini_fullstack/frontend/src/vite-env.d.ts +1 -0
- src/frontends/adk_gemini_fullstack/frontend/tsconfig.json +28 -0
- src/frontends/adk_gemini_fullstack/frontend/tsconfig.node.json +24 -0
- src/frontends/adk_gemini_fullstack/frontend/vite.config.ts +37 -0
- src/resources/locks/uv-adk_base-agent_engine.lock +24 -24
- src/resources/locks/uv-adk_base-cloud_run.lock +24 -24
- src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +3217 -0
- src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +3513 -0
- src/resources/locks/uv-agentic_rag-agent_engine.lock +88 -85
- src/resources/locks/uv-agentic_rag-cloud_run.lock +124 -119
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +94 -91
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +130 -125
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +91 -88
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +130 -125
- src/resources/locks/uv-live_api-cloud_run.lock +121 -116
- src/frontends/streamlit_adk/frontend/side_bar.py +0 -214
- src/frontends/streamlit_adk/frontend/streamlit_app.py +0 -314
- src/frontends/streamlit_adk/frontend/utils/chat_utils.py +0 -84
- src/frontends/streamlit_adk/frontend/utils/local_chat_history.py +0 -110
- src/frontends/streamlit_adk/frontend/utils/message_editing.py +0 -61
- src/frontends/streamlit_adk/frontend/utils/multimodal_utils.py +0 -223
- src/frontends/streamlit_adk/frontend/utils/stream_handler.py +0 -311
- src/frontends/streamlit_adk/frontend/utils/title_summary.py +0 -129
- {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"name": "frontend",
|
3
|
+
"private": true,
|
4
|
+
"version": "0.0.0",
|
5
|
+
"type": "module",
|
6
|
+
"scripts": {
|
7
|
+
"dev": "vite",
|
8
|
+
"build": "tsc -b && vite build",
|
9
|
+
"lint": "eslint .",
|
10
|
+
"preview": "vite preview"
|
11
|
+
},
|
12
|
+
"dependencies": {
|
13
|
+
"@langchain/core": "^0.3.55",
|
14
|
+
"@langchain/langgraph-sdk": "^0.0.74",
|
15
|
+
"@radix-ui/react-scroll-area": "^1.2.8",
|
16
|
+
"@radix-ui/react-select": "^2.2.4",
|
17
|
+
"@radix-ui/react-slot": "^1.2.2",
|
18
|
+
"@radix-ui/react-tabs": "^1.1.11",
|
19
|
+
"@radix-ui/react-tooltip": "^1.2.6",
|
20
|
+
"@tailwindcss/vite": "^4.1.5",
|
21
|
+
"class-variance-authority": "^0.7.1",
|
22
|
+
"clsx": "^2.1.1",
|
23
|
+
"lucide-react": "^0.508.0",
|
24
|
+
"react": "^19.0.0",
|
25
|
+
"react-dom": "^19.0.0",
|
26
|
+
"react-markdown": "^9.0.3",
|
27
|
+
"react-router-dom": "^7.5.3",
|
28
|
+
"tailwind-merge": "^3.2.0",
|
29
|
+
"tailwindcss": "^4.1.5"
|
30
|
+
},
|
31
|
+
"devDependencies": {
|
32
|
+
"@eslint/js": "^9.22.0",
|
33
|
+
"@types/node": "^22.15.17",
|
34
|
+
"@types/react": "^19.1.2",
|
35
|
+
"@types/react-dom": "^19.1.3",
|
36
|
+
"@vitejs/plugin-react-swc": "^3.9.0",
|
37
|
+
"eslint": "^9.22.0",
|
38
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
39
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
40
|
+
"globals": "^16.0.0",
|
41
|
+
"tw-animate-css": "^1.2.9",
|
42
|
+
"typescript": "~5.7.2",
|
43
|
+
"typescript-eslint": "^8.26.1",
|
44
|
+
"vite": "^6.3.4"
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
@@ -0,0 +1,565 @@
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
3
|
+
import { WelcomeScreen } from "@/components/WelcomeScreen";
|
4
|
+
import { ChatMessagesView } from "@/components/ChatMessagesView";
|
5
|
+
|
6
|
+
// Update DisplayData to be a string type
|
7
|
+
type DisplayData = string | null;
|
8
|
+
interface MessageWithAgent {
|
9
|
+
type: "human" | "ai";
|
10
|
+
content: string;
|
11
|
+
id: string;
|
12
|
+
agent?: string;
|
13
|
+
finalReportWithCitations?: boolean;
|
14
|
+
}
|
15
|
+
|
16
|
+
interface AgentMessage {
|
17
|
+
parts: { text: string }[];
|
18
|
+
role: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
interface AgentResponse {
|
22
|
+
content: AgentMessage;
|
23
|
+
usageMetadata: {
|
24
|
+
candidatesTokenCount: number;
|
25
|
+
promptTokenCount: number;
|
26
|
+
totalTokenCount: number;
|
27
|
+
};
|
28
|
+
author: string;
|
29
|
+
actions: {
|
30
|
+
stateDelta: {
|
31
|
+
research_plan?: string;
|
32
|
+
final_report_with_citations?: boolean;
|
33
|
+
};
|
34
|
+
};
|
35
|
+
}
|
36
|
+
|
37
|
+
interface ProcessedEvent {
|
38
|
+
title: string;
|
39
|
+
data: any;
|
40
|
+
}
|
41
|
+
|
42
|
+
export default function App() {
|
43
|
+
const [userId, setUserId] = useState<string | null>(null);
|
44
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
45
|
+
const [appName, setAppName] = useState<string | null>(null);
|
46
|
+
const [messages, setMessages] = useState<MessageWithAgent[]>([]);
|
47
|
+
const [displayData, setDisplayData] = useState<DisplayData | null>(null);
|
48
|
+
const [isLoading, setIsLoading] = useState(false);
|
49
|
+
const [messageEvents, setMessageEvents] = useState<Map<string, ProcessedEvent[]>>(new Map());
|
50
|
+
const [websiteCount, setWebsiteCount] = useState<number>(0);
|
51
|
+
const [isBackendReady, setIsBackendReady] = useState(false);
|
52
|
+
const [isCheckingBackend, setIsCheckingBackend] = useState(true);
|
53
|
+
const currentAgentRef = useRef('');
|
54
|
+
const accumulatedTextRef = useRef("");
|
55
|
+
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
56
|
+
|
57
|
+
const retryWithBackoff = async (
|
58
|
+
fn: () => Promise<any>,
|
59
|
+
maxRetries: number = 10,
|
60
|
+
maxDuration: number = 120000 // 2 minutes
|
61
|
+
): Promise<any> => {
|
62
|
+
const startTime = Date.now();
|
63
|
+
let lastError: Error;
|
64
|
+
|
65
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
66
|
+
if (Date.now() - startTime > maxDuration) {
|
67
|
+
throw new Error(`Retry timeout after ${maxDuration}ms`);
|
68
|
+
}
|
69
|
+
|
70
|
+
try {
|
71
|
+
return await fn();
|
72
|
+
} catch (error) {
|
73
|
+
lastError = error as Error;
|
74
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000); // Exponential backoff, max 5s
|
75
|
+
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error);
|
76
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
throw lastError!;
|
81
|
+
};
|
82
|
+
|
83
|
+
const createSession = async (): Promise<{userId: string, sessionId: string, appName: string}> => {
|
84
|
+
const generatedSessionId = uuidv4();
|
85
|
+
const response = await fetch(`/api/apps/app/users/u_999/sessions/${generatedSessionId}`, {
|
86
|
+
method: "POST",
|
87
|
+
headers: {
|
88
|
+
"Content-Type": "application/json"
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
if (!response.ok) {
|
93
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
|
94
|
+
}
|
95
|
+
|
96
|
+
const data = await response.json();
|
97
|
+
return {
|
98
|
+
userId: data.userId,
|
99
|
+
sessionId: data.id,
|
100
|
+
appName: data.appName
|
101
|
+
};
|
102
|
+
};
|
103
|
+
|
104
|
+
const checkBackendHealth = async (): Promise<boolean> => {
|
105
|
+
try {
|
106
|
+
// Use the docs endpoint or root endpoint to check if backend is ready
|
107
|
+
const response = await fetch("/api/docs", {
|
108
|
+
method: "GET",
|
109
|
+
headers: {
|
110
|
+
"Content-Type": "application/json"
|
111
|
+
}
|
112
|
+
});
|
113
|
+
return response.ok;
|
114
|
+
} catch (error) {
|
115
|
+
console.log("Backend not ready yet:", error);
|
116
|
+
return false;
|
117
|
+
}
|
118
|
+
};
|
119
|
+
|
120
|
+
// Function to extract text and metadata from SSE data
|
121
|
+
const extractDataFromSSE = (data: string) => {
|
122
|
+
try {
|
123
|
+
const parsed = JSON.parse(data);
|
124
|
+
console.log('[SSE PARSED EVENT]:', JSON.stringify(parsed, null, 2)); // DEBUG: Log parsed event
|
125
|
+
|
126
|
+
let textParts: string[] = [];
|
127
|
+
let agent = '';
|
128
|
+
let finalReportWithCitations = undefined;
|
129
|
+
let functionCall = null;
|
130
|
+
let functionResponse = null;
|
131
|
+
let sources = null;
|
132
|
+
|
133
|
+
// Check if content.parts exists and has text
|
134
|
+
if (parsed.content && parsed.content.parts) {
|
135
|
+
textParts = parsed.content.parts
|
136
|
+
.filter((part: any) => part.text)
|
137
|
+
.map((part: any) => part.text);
|
138
|
+
|
139
|
+
// Check for function calls
|
140
|
+
const functionCallPart = parsed.content.parts.find((part: any) => part.functionCall);
|
141
|
+
if (functionCallPart) {
|
142
|
+
functionCall = functionCallPart.functionCall;
|
143
|
+
}
|
144
|
+
|
145
|
+
// Check for function responses
|
146
|
+
const functionResponsePart = parsed.content.parts.find((part: any) => part.functionResponse);
|
147
|
+
if (functionResponsePart) {
|
148
|
+
functionResponse = functionResponsePart.functionResponse;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
// Extract agent information
|
153
|
+
if (parsed.author) {
|
154
|
+
agent = parsed.author;
|
155
|
+
console.log('[SSE EXTRACT] Agent:', agent); // DEBUG: Log agent
|
156
|
+
}
|
157
|
+
|
158
|
+
if (
|
159
|
+
parsed.actions &&
|
160
|
+
parsed.actions.stateDelta &&
|
161
|
+
parsed.actions.stateDelta.final_report_with_citations
|
162
|
+
) {
|
163
|
+
finalReportWithCitations = parsed.actions.stateDelta.final_report_with_citations;
|
164
|
+
}
|
165
|
+
|
166
|
+
// Extract website count from research agents
|
167
|
+
let sourceCount = 0;
|
168
|
+
if ((parsed.author === 'section_researcher' || parsed.author === 'enhanced_search_executor')) {
|
169
|
+
console.log('[SSE EXTRACT] Relevant agent for source count:', parsed.author); // DEBUG
|
170
|
+
if (parsed.actions?.stateDelta?.url_to_short_id) {
|
171
|
+
console.log('[SSE EXTRACT] url_to_short_id found:', parsed.actions.stateDelta.url_to_short_id); // DEBUG
|
172
|
+
sourceCount = Object.keys(parsed.actions.stateDelta.url_to_short_id).length;
|
173
|
+
console.log('[SSE EXTRACT] Calculated sourceCount:', sourceCount); // DEBUG
|
174
|
+
} else {
|
175
|
+
console.log('[SSE EXTRACT] url_to_short_id NOT found for agent:', parsed.author); // DEBUG
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
// Extract sources if available
|
180
|
+
if (parsed.actions?.stateDelta?.sources) {
|
181
|
+
sources = parsed.actions.stateDelta.sources;
|
182
|
+
console.log('[SSE EXTRACT] Sources found:', sources); // DEBUG
|
183
|
+
}
|
184
|
+
|
185
|
+
|
186
|
+
return { textParts, agent, finalReportWithCitations, functionCall, functionResponse, sourceCount, sources };
|
187
|
+
} catch (error) {
|
188
|
+
// Log the error and a truncated version of the problematic data for easier debugging.
|
189
|
+
const truncatedData = data.length > 200 ? data.substring(0, 200) + "..." : data;
|
190
|
+
console.error('Error parsing SSE data. Raw data (truncated): "', truncatedData, '". Error details:', error);
|
191
|
+
return { textParts: [], agent: '', finalReportWithCitations: undefined, functionCall: null, functionResponse: null, sourceCount: 0, sources: null };
|
192
|
+
}
|
193
|
+
};
|
194
|
+
|
195
|
+
// Define getEventTitle here or ensure it's in scope from where it's used
|
196
|
+
const getEventTitle = (agentName: string): string => {
|
197
|
+
switch (agentName) {
|
198
|
+
case "plan_generator":
|
199
|
+
return "Planning Research Strategy";
|
200
|
+
case "section_planner":
|
201
|
+
return "Structuring Report Outline";
|
202
|
+
case "section_researcher":
|
203
|
+
return "Initial Web Research";
|
204
|
+
case "research_evaluator":
|
205
|
+
return "Evaluating Research Quality";
|
206
|
+
case "EscalationChecker":
|
207
|
+
return "Quality Assessment";
|
208
|
+
case "enhanced_search_executor":
|
209
|
+
return "Enhanced Web Research";
|
210
|
+
case "research_pipeline":
|
211
|
+
return "Executing Research Pipeline";
|
212
|
+
case "iterative_refinement_loop":
|
213
|
+
return "Refining Research";
|
214
|
+
case "interactive_planner_agent":
|
215
|
+
case "root_agent":
|
216
|
+
return "Interactive Planning";
|
217
|
+
default:
|
218
|
+
return `Processing (${agentName || 'Unknown Agent'})`;
|
219
|
+
}
|
220
|
+
};
|
221
|
+
|
222
|
+
const processSseEventData = (jsonData: string, aiMessageId: string) => {
|
223
|
+
const { textParts, agent, finalReportWithCitations, functionCall, functionResponse, sourceCount, sources } = extractDataFromSSE(jsonData);
|
224
|
+
|
225
|
+
if (sourceCount > 0) {
|
226
|
+
console.log('[SSE HANDLER] Updating websiteCount. Current sourceCount:', sourceCount);
|
227
|
+
setWebsiteCount(prev => Math.max(prev, sourceCount));
|
228
|
+
}
|
229
|
+
|
230
|
+
if (agent && agent !== currentAgentRef.current) {
|
231
|
+
currentAgentRef.current = agent;
|
232
|
+
}
|
233
|
+
|
234
|
+
if (functionCall) {
|
235
|
+
const functionCallTitle = `Function Call: ${functionCall.name}`;
|
236
|
+
console.log('[SSE HANDLER] Adding Function Call timeline event:', functionCallTitle);
|
237
|
+
setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
|
238
|
+
title: functionCallTitle,
|
239
|
+
data: { type: 'functionCall', name: functionCall.name, args: functionCall.args, id: functionCall.id }
|
240
|
+
}]));
|
241
|
+
}
|
242
|
+
|
243
|
+
if (functionResponse) {
|
244
|
+
const functionResponseTitle = `Function Response: ${functionResponse.name}`;
|
245
|
+
console.log('[SSE HANDLER] Adding Function Response timeline event:', functionResponseTitle);
|
246
|
+
setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
|
247
|
+
title: functionResponseTitle,
|
248
|
+
data: { type: 'functionResponse', name: functionResponse.name, response: functionResponse.response, id: functionResponse.id }
|
249
|
+
}]));
|
250
|
+
}
|
251
|
+
|
252
|
+
if (textParts.length > 0 && agent !== "report_composer_with_citations") {
|
253
|
+
if (agent !== "interactive_planner_agent") {
|
254
|
+
const eventTitle = getEventTitle(agent);
|
255
|
+
console.log('[SSE HANDLER] Adding Text timeline event for agent:', agent, 'Title:', eventTitle, 'Data:', textParts.join(" "));
|
256
|
+
setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
|
257
|
+
title: eventTitle,
|
258
|
+
data: { type: 'text', content: textParts.join(" ") }
|
259
|
+
}]));
|
260
|
+
} else { // interactive_planner_agent text updates the main AI message
|
261
|
+
for (const text of textParts) {
|
262
|
+
accumulatedTextRef.current += text + " ";
|
263
|
+
setMessages(prev => prev.map(msg =>
|
264
|
+
msg.id === aiMessageId ? { ...msg, content: accumulatedTextRef.current.trim(), agent: currentAgentRef.current || msg.agent } : msg
|
265
|
+
));
|
266
|
+
setDisplayData(accumulatedTextRef.current.trim());
|
267
|
+
}
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
if (sources) {
|
272
|
+
console.log('[SSE HANDLER] Adding Retrieved Sources timeline event:', sources);
|
273
|
+
setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
|
274
|
+
title: "Retrieved Sources", data: { type: 'sources', content: sources }
|
275
|
+
}]));
|
276
|
+
}
|
277
|
+
|
278
|
+
if (agent === "report_composer_with_citations" && finalReportWithCitations) {
|
279
|
+
const finalReportMessageId = Date.now().toString() + "_final";
|
280
|
+
setMessages(prev => [...prev, { type: "ai", content: finalReportWithCitations as string, id: finalReportMessageId, agent: currentAgentRef.current, finalReportWithCitations: true }]);
|
281
|
+
setDisplayData(finalReportWithCitations as string);
|
282
|
+
}
|
283
|
+
};
|
284
|
+
|
285
|
+
const handleSubmit = useCallback(async (query: string, model: string, effort: string) => {
|
286
|
+
if (!query.trim()) return;
|
287
|
+
|
288
|
+
setIsLoading(true);
|
289
|
+
try {
|
290
|
+
// Create session if it doesn't exist
|
291
|
+
let currentUserId = userId;
|
292
|
+
let currentSessionId = sessionId;
|
293
|
+
let currentAppName = appName;
|
294
|
+
|
295
|
+
if (!currentSessionId || !currentUserId || !currentAppName) {
|
296
|
+
console.log('Creating new session...');
|
297
|
+
const sessionData = await retryWithBackoff(createSession);
|
298
|
+
currentUserId = sessionData.userId;
|
299
|
+
currentSessionId = sessionData.sessionId;
|
300
|
+
currentAppName = sessionData.appName;
|
301
|
+
|
302
|
+
setUserId(currentUserId);
|
303
|
+
setSessionId(currentSessionId);
|
304
|
+
setAppName(currentAppName);
|
305
|
+
console.log('Session created successfully:', { currentUserId, currentSessionId, currentAppName });
|
306
|
+
}
|
307
|
+
|
308
|
+
// Add user message to chat
|
309
|
+
const userMessageId = Date.now().toString();
|
310
|
+
setMessages(prev => [...prev, { type: "human", content: query, id: userMessageId }]);
|
311
|
+
|
312
|
+
// Create AI message placeholder
|
313
|
+
const aiMessageId = Date.now().toString() + "_ai";
|
314
|
+
currentAgentRef.current = ''; // Reset current agent
|
315
|
+
accumulatedTextRef.current = ''; // Reset accumulated text
|
316
|
+
|
317
|
+
setMessages(prev => [...prev, {
|
318
|
+
type: "ai",
|
319
|
+
content: "",
|
320
|
+
id: aiMessageId,
|
321
|
+
agent: '',
|
322
|
+
}]);
|
323
|
+
|
324
|
+
// Send the message with retry logic
|
325
|
+
const sendMessage = async () => {
|
326
|
+
const response = await fetch("/api/run_sse", {
|
327
|
+
method: "POST",
|
328
|
+
headers: {
|
329
|
+
"Content-Type": "application/json",
|
330
|
+
},
|
331
|
+
body: JSON.stringify({
|
332
|
+
appName: currentAppName,
|
333
|
+
userId: currentUserId,
|
334
|
+
sessionId: currentSessionId,
|
335
|
+
newMessage: {
|
336
|
+
parts: [{ text: query }],
|
337
|
+
role: "user"
|
338
|
+
},
|
339
|
+
streaming: false
|
340
|
+
}),
|
341
|
+
});
|
342
|
+
|
343
|
+
if (!response.ok) {
|
344
|
+
throw new Error(`Failed to send message: ${response.status} ${response.statusText}`);
|
345
|
+
}
|
346
|
+
|
347
|
+
return response;
|
348
|
+
};
|
349
|
+
|
350
|
+
const response = await retryWithBackoff(sendMessage);
|
351
|
+
|
352
|
+
// Handle SSE streaming
|
353
|
+
const reader = response.body?.getReader();
|
354
|
+
const decoder = new TextDecoder();
|
355
|
+
let lineBuffer = "";
|
356
|
+
let eventDataBuffer = "";
|
357
|
+
|
358
|
+
if (reader) {
|
359
|
+
// eslint-disable-next-line no-constant-condition
|
360
|
+
while (true) {
|
361
|
+
const { done, value } = await reader.read();
|
362
|
+
|
363
|
+
if (value) {
|
364
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
365
|
+
}
|
366
|
+
|
367
|
+
let eolIndex;
|
368
|
+
// Process all complete lines in the buffer, or the remaining buffer if 'done'
|
369
|
+
while ((eolIndex = lineBuffer.indexOf('\n')) >= 0 || (done && lineBuffer.length > 0)) {
|
370
|
+
let line: string;
|
371
|
+
if (eolIndex >= 0) {
|
372
|
+
line = lineBuffer.substring(0, eolIndex);
|
373
|
+
lineBuffer = lineBuffer.substring(eolIndex + 1);
|
374
|
+
} else { // Only if done and lineBuffer has content without a trailing newline
|
375
|
+
line = lineBuffer;
|
376
|
+
lineBuffer = "";
|
377
|
+
}
|
378
|
+
|
379
|
+
if (line.trim() === "") { // Empty line: dispatch event
|
380
|
+
if (eventDataBuffer.length > 0) {
|
381
|
+
// Remove trailing newline before parsing
|
382
|
+
const jsonDataToParse = eventDataBuffer.endsWith('\n') ? eventDataBuffer.slice(0, -1) : eventDataBuffer;
|
383
|
+
console.log('[SSE DISPATCH EVENT]:', jsonDataToParse.substring(0, 200) + "..."); // DEBUG
|
384
|
+
processSseEventData(jsonDataToParse, aiMessageId);
|
385
|
+
eventDataBuffer = ""; // Reset for next event
|
386
|
+
}
|
387
|
+
} else if (line.startsWith('data:')) {
|
388
|
+
eventDataBuffer += line.substring(5).trimStart() + '\n'; // Add newline as per spec for multi-line data
|
389
|
+
} else if (line.startsWith(':')) {
|
390
|
+
// Comment line, ignore
|
391
|
+
} // Other SSE fields (event, id, retry) can be handled here if needed
|
392
|
+
}
|
393
|
+
|
394
|
+
if (done) {
|
395
|
+
// If the loop exited due to 'done', and there's still data in eventDataBuffer
|
396
|
+
// (e.g., stream ended after data lines but before an empty line delimiter)
|
397
|
+
if (eventDataBuffer.length > 0) {
|
398
|
+
const jsonDataToParse = eventDataBuffer.endsWith('\n') ? eventDataBuffer.slice(0, -1) : eventDataBuffer;
|
399
|
+
console.log('[SSE DISPATCH FINAL EVENT]:', jsonDataToParse.substring(0,200) + "..."); // DEBUG
|
400
|
+
processSseEventData(jsonDataToParse, aiMessageId);
|
401
|
+
eventDataBuffer = ""; // Clear buffer
|
402
|
+
}
|
403
|
+
break; // Exit the while(true) loop
|
404
|
+
}
|
405
|
+
}
|
406
|
+
}
|
407
|
+
|
408
|
+
setIsLoading(false);
|
409
|
+
|
410
|
+
} catch (error) {
|
411
|
+
console.error("Error:", error);
|
412
|
+
// Update the AI message placeholder with an error message
|
413
|
+
const aiMessageId = Date.now().toString() + "_ai_error";
|
414
|
+
setMessages(prev => [...prev, {
|
415
|
+
type: "ai",
|
416
|
+
content: `Sorry, there was an error processing your request: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
417
|
+
id: aiMessageId
|
418
|
+
}]);
|
419
|
+
setIsLoading(false);
|
420
|
+
}
|
421
|
+
}, [processSseEventData]);
|
422
|
+
|
423
|
+
useEffect(() => {
|
424
|
+
if (scrollAreaRef.current) {
|
425
|
+
const scrollViewport = scrollAreaRef.current.querySelector(
|
426
|
+
"[data-radix-scroll-area-viewport]"
|
427
|
+
);
|
428
|
+
if (scrollViewport) {
|
429
|
+
scrollViewport.scrollTop = scrollViewport.scrollHeight;
|
430
|
+
}
|
431
|
+
}
|
432
|
+
}, [messages]);
|
433
|
+
|
434
|
+
useEffect(() => {
|
435
|
+
const checkBackend = async () => {
|
436
|
+
setIsCheckingBackend(true);
|
437
|
+
|
438
|
+
// Check if backend is ready with retry logic
|
439
|
+
const maxAttempts = 60; // 2 minutes with 2-second intervals
|
440
|
+
let attempts = 0;
|
441
|
+
|
442
|
+
while (attempts < maxAttempts) {
|
443
|
+
const isReady = await checkBackendHealth();
|
444
|
+
if (isReady) {
|
445
|
+
setIsBackendReady(true);
|
446
|
+
setIsCheckingBackend(false);
|
447
|
+
return;
|
448
|
+
}
|
449
|
+
|
450
|
+
attempts++;
|
451
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds between checks
|
452
|
+
}
|
453
|
+
|
454
|
+
// If we get here, backend didn't come up in time
|
455
|
+
setIsCheckingBackend(false);
|
456
|
+
console.error("Backend failed to start within 2 minutes");
|
457
|
+
};
|
458
|
+
|
459
|
+
checkBackend();
|
460
|
+
}, []);
|
461
|
+
|
462
|
+
const handleCancel = useCallback(() => {
|
463
|
+
setMessages([]);
|
464
|
+
setDisplayData(null);
|
465
|
+
setMessageEvents(new Map());
|
466
|
+
setWebsiteCount(0);
|
467
|
+
window.location.reload();
|
468
|
+
}, []);
|
469
|
+
|
470
|
+
// Scroll to bottom when messages update
|
471
|
+
const scrollToBottom = useCallback(() => {
|
472
|
+
if (scrollAreaRef.current) {
|
473
|
+
const scrollViewport = scrollAreaRef.current.querySelector(
|
474
|
+
"[data-radix-scroll-area-viewport]"
|
475
|
+
);
|
476
|
+
if (scrollViewport) {
|
477
|
+
scrollViewport.scrollTop = scrollViewport.scrollHeight;
|
478
|
+
}
|
479
|
+
}
|
480
|
+
}, []);
|
481
|
+
|
482
|
+
const BackendLoadingScreen = () => (
|
483
|
+
<div className="flex-1 flex flex-col items-center justify-center p-4 overflow-hidden relative">
|
484
|
+
<div className="w-full max-w-2xl z-10
|
485
|
+
bg-neutral-900/50 backdrop-blur-md
|
486
|
+
p-8 rounded-2xl border border-neutral-700
|
487
|
+
shadow-2xl shadow-black/60">
|
488
|
+
|
489
|
+
<div className="text-center space-y-6">
|
490
|
+
<h1 className="text-4xl font-bold text-white flex items-center justify-center gap-3">
|
491
|
+
✨ Gemini FullStack - ADK 🚀
|
492
|
+
</h1>
|
493
|
+
|
494
|
+
<div className="flex flex-col items-center space-y-4">
|
495
|
+
{/* Spinning animation */}
|
496
|
+
<div className="relative">
|
497
|
+
<div className="w-16 h-16 border-4 border-neutral-600 border-t-blue-500 rounded-full animate-spin"></div>
|
498
|
+
<div className="absolute inset-0 w-16 h-16 border-4 border-transparent border-r-purple-500 rounded-full animate-spin" style={{animationDirection: 'reverse', animationDuration: '1.5s'}}></div>
|
499
|
+
</div>
|
500
|
+
|
501
|
+
<div className="space-y-2">
|
502
|
+
<p className="text-xl text-neutral-300">
|
503
|
+
Waiting for backend to be ready...
|
504
|
+
</p>
|
505
|
+
<p className="text-sm text-neutral-400">
|
506
|
+
This may take a moment on first startup
|
507
|
+
</p>
|
508
|
+
</div>
|
509
|
+
|
510
|
+
{/* Animated dots */}
|
511
|
+
<div className="flex space-x-1">
|
512
|
+
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{animationDelay: '0ms'}}></div>
|
513
|
+
<div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{animationDelay: '150ms'}}></div>
|
514
|
+
<div className="w-2 h-2 bg-pink-500 rounded-full animate-bounce" style={{animationDelay: '300ms'}}></div>
|
515
|
+
</div>
|
516
|
+
</div>
|
517
|
+
</div>
|
518
|
+
</div>
|
519
|
+
</div>
|
520
|
+
);
|
521
|
+
|
522
|
+
return (
|
523
|
+
<div className="flex h-screen bg-neutral-800 text-neutral-100 font-sans antialiased">
|
524
|
+
<main className="flex-1 flex flex-col overflow-hidden w-full">
|
525
|
+
<div className={`flex-1 overflow-y-auto ${(messages.length === 0 || isCheckingBackend) ? "flex" : ""}`}>
|
526
|
+
{isCheckingBackend ? (
|
527
|
+
<BackendLoadingScreen />
|
528
|
+
) : !isBackendReady ? (
|
529
|
+
<div className="flex-1 flex flex-col items-center justify-center p-4">
|
530
|
+
<div className="text-center space-y-4">
|
531
|
+
<h2 className="text-2xl font-bold text-red-400">Backend Unavailable</h2>
|
532
|
+
<p className="text-neutral-300">
|
533
|
+
Unable to connect to backend services at localhost:8000
|
534
|
+
</p>
|
535
|
+
<button
|
536
|
+
onClick={() => window.location.reload()}
|
537
|
+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
538
|
+
>
|
539
|
+
Retry
|
540
|
+
</button>
|
541
|
+
</div>
|
542
|
+
</div>
|
543
|
+
) : messages.length === 0 ? (
|
544
|
+
<WelcomeScreen
|
545
|
+
handleSubmit={handleSubmit}
|
546
|
+
isLoading={isLoading}
|
547
|
+
onCancel={handleCancel}
|
548
|
+
/>
|
549
|
+
) : (
|
550
|
+
<ChatMessagesView
|
551
|
+
messages={messages}
|
552
|
+
isLoading={isLoading}
|
553
|
+
scrollAreaRef={scrollAreaRef}
|
554
|
+
onSubmit={handleSubmit}
|
555
|
+
onCancel={handleCancel}
|
556
|
+
displayData={displayData}
|
557
|
+
messageEvents={messageEvents}
|
558
|
+
websiteCount={websiteCount}
|
559
|
+
/>
|
560
|
+
)}
|
561
|
+
</div>
|
562
|
+
</main>
|
563
|
+
</div>
|
564
|
+
);
|
565
|
+
}
|