fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__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.
Files changed (71) hide show
  1. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
  2. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
  3. fastapi_gen/__init__.py +6 -1
  4. fastapi_gen/cli.py +9 -0
  5. fastapi_gen/config.py +154 -2
  6. fastapi_gen/generator.py +34 -14
  7. fastapi_gen/prompts.py +172 -31
  8. fastapi_gen/template/VARIABLES.md +33 -4
  9. fastapi_gen/template/cookiecutter.json +10 -0
  10. fastapi_gen/template/hooks/post_gen_project.py +87 -2
  11. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
  12. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
  69. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
  70. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
  71. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useState } from "react";
3
+ import { useCallback, useRef, useState } from "react";
4
4
  import { nanoid } from "nanoid";
5
5
  import { useWebSocket } from "./use-websocket";
6
6
  import { useChatStore } from "@/stores";
7
- import type { ChatMessage, ToolCall, WSEvent } from "@/types";
7
+ import type { ChatMessage, ToolCall, WSEvent, PendingApproval, Decision } from "@/types";
8
8
  import { WS_URL } from "@/lib/constants";
9
9
  {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
10
10
  import { useConversationStore } from "@/stores";
@@ -33,11 +33,39 @@ export function useChat() {
33
33
 
34
34
  const [isProcessing, setIsProcessing] = useState(false);
35
35
  const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
36
+ // Use ref for groupId to avoid React state timing issues with rapid WebSocket events
37
+ const currentGroupIdRef = useRef<string | null>(null);
38
+ // Human-in-the-Loop: pending tool approval state
39
+ const [pendingApproval, setPendingApproval] = useState<PendingApproval | null>(null);
36
40
 
37
41
  const handleWebSocketMessage = useCallback(
38
42
  (event: MessageEvent) => {
39
43
  const wsEvent: WSEvent = JSON.parse(event.data);
40
44
 
45
+ // Helper to create a new message
46
+ const createNewMessage = (content: string): string => {
47
+ // Mark previous message as not streaming before creating new one
48
+ if (currentMessageId) {
49
+ updateMessage(currentMessageId, (msg) => ({
50
+ ...msg,
51
+ isStreaming: false,
52
+ }));
53
+ }
54
+
55
+ const newMsgId = nanoid();
56
+ addMessage({
57
+ id: newMsgId,
58
+ role: "assistant",
59
+ content,
60
+ timestamp: new Date(),
61
+ isStreaming: true,
62
+ toolCalls: [],
63
+ groupId: currentGroupIdRef.current || undefined,
64
+ });
65
+ setCurrentMessageId(newMsgId);
66
+ return newMsgId;
67
+ };
68
+
41
69
  switch (wsEvent.type) {
42
70
  {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
43
71
  case "conversation_created": {
@@ -56,17 +84,15 @@ export function useChat() {
56
84
  {%- endif %}
57
85
 
58
86
  case "model_request_start": {
59
- // Create new assistant message placeholder
60
- const newMsgId = nanoid();
61
- setCurrentMessageId(newMsgId);
62
- addMessage({
63
- id: newMsgId,
64
- role: "assistant",
65
- content: "",
66
- timestamp: new Date(),
67
- isStreaming: true,
68
- toolCalls: [],
69
- });
87
+ // PydanticAI/LangChain - create message immediately
88
+ createNewMessage("");
89
+ break;
90
+ }
91
+
92
+ case "crew_start":
93
+ case "crew_started": {
94
+ // CrewAI - generate groupId for this execution, wait for agent events
95
+ currentGroupIdRef.current = nanoid();
70
96
  break;
71
97
  }
72
98
 
@@ -82,6 +108,118 @@ export function useChat() {
82
108
  break;
83
109
  }
84
110
 
111
+ // CrewAI agent events - each agent gets its own message container
112
+ case "agent_started": {
113
+ const { agent } = wsEvent.data as {
114
+ agent: string;
115
+ task: string;
116
+ };
117
+ // Create NEW message for this agent (groupId read from ref)
118
+ createNewMessage(`🤖 **${agent}** is starting...`);
119
+ break;
120
+ }
121
+
122
+ case "agent_completed": {
123
+ // Finalize current agent's message with output
124
+ if (currentMessageId) {
125
+ const { agent, output } = wsEvent.data as {
126
+ agent: string;
127
+ output: string;
128
+ };
129
+ updateMessage(currentMessageId, (msg) => ({
130
+ ...msg,
131
+ content: `✅ **${agent}**\n\n${output}`,
132
+ isStreaming: false,
133
+ }));
134
+ }
135
+ break;
136
+ }
137
+
138
+ // CrewAI task events - create separate message for each task
139
+ case "task_started": {
140
+ const { description, agent } = wsEvent.data as {
141
+ task_id: string;
142
+ description: string;
143
+ agent: string;
144
+ };
145
+ // Create NEW message for this task (groupId read from ref)
146
+ createNewMessage(`📋 **Task** (${agent})\n\n${description}`);
147
+ break;
148
+ }
149
+
150
+ case "task_completed": {
151
+ // Finalize the task message
152
+ if (currentMessageId) {
153
+ const { output, agent } = wsEvent.data as {
154
+ task_id: string;
155
+ output: string;
156
+ agent: string;
157
+ };
158
+ updateMessage(currentMessageId, (msg) => ({
159
+ ...msg,
160
+ content: `✅ **Task completed** (${agent})\n\n${output}`,
161
+ isStreaming: false,
162
+ }));
163
+ }
164
+ break;
165
+ }
166
+
167
+ // CrewAI tool events
168
+ case "tool_started": {
169
+ if (currentMessageId) {
170
+ const { tool_name, tool_args, agent } = wsEvent.data as {
171
+ tool_name: string;
172
+ tool_args: string;
173
+ agent: string;
174
+ };
175
+ const toolCall: ToolCall = {
176
+ id: nanoid(),
177
+ name: tool_name,
178
+ args: { input: tool_args, agent },
179
+ status: "running",
180
+ };
181
+ addToolCall(currentMessageId, toolCall);
182
+ }
183
+ break;
184
+ }
185
+
186
+ case "tool_finished": {
187
+ // Tool finished - update last tool call status
188
+ if (currentMessageId) {
189
+ const { tool_name, tool_result } = wsEvent.data as {
190
+ tool_name: string;
191
+ tool_result: string;
192
+ agent: string;
193
+ };
194
+ // Find and update the matching tool call
195
+ updateMessage(currentMessageId, (msg) => {
196
+ const toolCalls = msg.toolCalls || [];
197
+ const lastToolCall = toolCalls.find(
198
+ (tc) => tc.name === tool_name && tc.status === "running"
199
+ );
200
+ if (lastToolCall) {
201
+ return {
202
+ ...msg,
203
+ toolCalls: toolCalls.map((tc) =>
204
+ tc.id === lastToolCall.id
205
+ ? { ...tc, result: tool_result, status: "completed" as const }
206
+ : tc
207
+ ),
208
+ };
209
+ }
210
+ return msg;
211
+ });
212
+ }
213
+ break;
214
+ }
215
+
216
+ // LLM events (can be used for showing thinking status)
217
+ case "llm_started":
218
+ case "llm_completed": {
219
+ // LLM lifecycle events - optionally show status
220
+ break;
221
+ }
222
+
85
223
  case "tool_call": {
86
224
  // Add tool call to current message
87
225
  if (currentMessageId) {
@@ -119,22 +257,33 @@ export function useChat() {
119
257
  case "final_result": {
120
258
  // Finalize message
121
259
  if (currentMessageId) {
122
- updateMessage(currentMessageId, (msg) => ({
123
- ...msg,
124
- isStreaming: false,
125
- }));
260
+ const { output } = wsEvent.data as { output: string };
261
+ if (output) {
262
+ updateMessage(currentMessageId, (msg) => ({
263
+ ...msg,
264
+ content: msg.content || output,
265
+ isStreaming: false,
266
+ }));
267
+ } else {
268
+ updateMessage(currentMessageId, (msg) => ({
269
+ ...msg,
270
+ isStreaming: false,
271
+ }));
272
+ }
126
273
  }
127
274
  setIsProcessing(false);
128
275
  setCurrentMessageId(null);
276
+ currentGroupIdRef.current = null;
129
277
  break;
130
278
  }
131
279
 
132
280
  case "error": {
133
281
  // Handle error
134
282
  if (currentMessageId) {
283
+ const { message } = wsEvent.data as { message: string };
135
284
  updateMessage(currentMessageId, (msg) => ({
136
285
  ...msg,
137
- content: msg.content + "\n\n[Error occurred]",
286
+ content: msg.content + `\n\nError: ${message || "Unknown error"}`,
138
287
  isStreaming: false,
139
288
  }));
140
289
  }
@@ -142,6 +291,35 @@ export function useChat() {
142
291
  break;
143
292
  }
144
293
 
294
+ case "tool_approval_required": {
295
+ // Human-in-the-Loop: AI wants to execute tools that need approval
296
+ const { action_requests, review_configs } = wsEvent.data as {
297
+ action_requests: Array<{
298
+ id: string;
299
+ tool_name: string;
300
+ args: Record<string, unknown>;
301
+ }>;
302
+ review_configs: Array<{
303
+ tool_name: string;
304
+ allow_edit?: boolean;
305
+ timeout?: number;
306
+ }>;
307
+ };
308
+ setPendingApproval({
309
+ actionRequests: action_requests,
310
+ reviewConfigs: review_configs,
311
+ });
312
+ // Show pending tools in the current message
313
+ if (currentMessageId) {
314
+ const toolNames = action_requests.map((ar) => ar.tool_name).join(", ");
315
+ updateMessage(currentMessageId, (msg) => ({
316
+ ...msg,
317
+ content: msg.content + `\n\n⏸️ Waiting for approval: ${toolNames}`,
318
+ }));
319
+ }
320
+ break;
321
+ }
322
+
145
323
  case "complete": {
146
324
  setIsProcessing(false);
147
325
  break;
@@ -191,6 +369,49 @@ export function useChat() {
191
369
  {%- endif %}
192
370
  );
193
371
 
372
+ // Human-in-the-Loop: send resume message with user decisions
373
+ const sendResumeDecisions = useCallback(
374
+ (decisions: Decision[]) => {
375
+ // Clear pending approval state
376
+ setPendingApproval(null);
377
+
378
+ // Update message to show decisions were made
379
+ if (currentMessageId) {
380
+ const approvedCount = decisions.filter((d) => d.type === "approve").length;
381
+ const editedCount = decisions.filter((d) => d.type === "edit").length;
382
+ const rejectedCount = decisions.filter((d) => d.type === "reject").length;
383
+
384
+ const summaryParts: string[] = [];
385
+ if (approvedCount > 0) summaryParts.push(`${approvedCount} approved`);
386
+ if (editedCount > 0) summaryParts.push(`${editedCount} edited`);
387
+ if (rejectedCount > 0) summaryParts.push(`${rejectedCount} rejected`);
388
+
389
+ updateMessage(currentMessageId, (msg) => ({
390
+ ...msg,
391
+ content: msg.content.replace(
392
+ /\n\n⏸️ Waiting for approval:.*$/,
393
+ `\n\n✅ Decisions: ${summaryParts.join(", ")}`
394
+ ),
395
+ }));
396
+ }
397
+
398
+ // Send resume message to WebSocket
399
+ sendMessage({
400
+ type: "resume",
401
+ decisions: decisions.map((d) => {
402
+ if (d.type === "edit" && d.editedAction) {
403
+ return {
404
+ type: "edit",
405
+ edited_action: d.editedAction,
406
+ };
407
+ }
408
+ return { type: d.type };
409
+ }),
410
+ });
411
+ },
412
+ [currentMessageId, updateMessage, sendMessage]
413
+ );
414
+
194
415
  return {
195
416
  messages,
196
417
  isConnected,
@@ -199,5 +420,8 @@ export function useChat() {
199
420
  disconnect,
200
421
  sendMessage: sendChatMessage,
201
422
  clearMessages,
423
+ // Human-in-the-Loop support
424
+ pendingApproval,
425
+ sendResumeDecisions,
202
426
  };
203
427
  }
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useState } from "react";
3
+ import { useCallback, useRef, useState } from "react";
4
4
  import { nanoid } from "nanoid";
5
5
  import { useWebSocket } from "./use-websocket";
6
6
  import { useLocalChatStore } from "@/stores/local-chat-store";
7
- import type { ChatMessage, ToolCall, WSEvent } from "@/types";
7
+ import type { ChatMessage, ToolCall, WSEvent, PendingApproval, Decision } from "@/types";
8
8
  import { WS_URL } from "@/lib/constants";
9
9
 
10
10
  export function useLocalChat() {
@@ -22,23 +22,50 @@ export function useLocalChat() {
22
22
  const messages = getCurrentMessages();
23
23
  const [isProcessing, setIsProcessing] = useState(false);
24
24
  const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
25
+ // Use ref for groupId to avoid React state timing issues with rapid WebSocket events
26
+ const currentGroupIdRef = useRef<string | null>(null);
27
+ // Human-in-the-Loop: pending tool approval state
28
+ const [pendingApproval, setPendingApproval] = useState<PendingApproval | null>(null);
25
29
 
26
30
  const handleWebSocketMessage = useCallback(
27
31
  (event: MessageEvent) => {
28
32
  const wsEvent: WSEvent = JSON.parse(event.data);
29
33
 
34
+ // Helper to create a new message for CrewAI events
35
+ const createNewMessage = (content: string): string => {
36
+ // Mark previous message as not streaming before creating new one
37
+ if (currentMessageId) {
38
+ updateMessage(currentMessageId, (msg) => ({
39
+ ...msg,
40
+ isStreaming: false,
41
+ }));
42
+ }
43
+
44
+ const newMsgId = nanoid();
45
+ addMessage({
46
+ id: newMsgId,
47
+ role: "assistant",
48
+ content,
49
+ timestamp: new Date(),
50
+ isStreaming: true,
51
+ toolCalls: [],
52
+ groupId: currentGroupIdRef.current || undefined,
53
+ });
54
+ setCurrentMessageId(newMsgId);
55
+ return newMsgId;
56
+ };
57
+
30
58
  switch (wsEvent.type) {
31
59
  case "model_request_start": {
32
- const newMsgId = nanoid();
33
- setCurrentMessageId(newMsgId);
34
- addMessage({
35
- id: newMsgId,
36
- role: "assistant",
37
- content: "",
38
- timestamp: new Date(),
39
- isStreaming: true,
40
- toolCalls: [],
41
- });
60
+ // PydanticAI/LangChain - create message immediately
61
+ createNewMessage("");
62
+ break;
63
+ }
64
+
65
+ case "crew_start":
66
+ case "crew_started": {
67
+ // CrewAI - generate groupId for this execution, wait for agent events
68
+ currentGroupIdRef.current = nanoid();
42
69
  break;
43
70
  }
44
71
 
@@ -54,6 +81,117 @@ export function useLocalChat() {
54
81
  break;
55
82
  }
56
83
 
84
+ // CrewAI agent events - each agent gets its own message container
85
+ case "agent_started": {
86
+ const { agent } = wsEvent.data as {
87
+ agent: string;
88
+ task: string;
89
+ };
90
+ // Create NEW message for this agent (groupId read from ref)
91
+ createNewMessage(`🤖 **${agent}** is starting...`);
92
+ break;
93
+ }
94
+
95
+ case "agent_completed": {
96
+ // Finalize current agent's message with output
97
+ if (currentMessageId) {
98
+ const { agent, output } = wsEvent.data as {
99
+ agent: string;
100
+ output: string;
101
+ };
102
+ updateMessage(currentMessageId, (msg) => ({
103
+ ...msg,
104
+ content: `✅ **${agent}**\n\n${output}`,
105
+ isStreaming: false,
106
+ }));
107
+ }
108
+ break;
109
+ }
110
+
111
+ // CrewAI task events - create separate message for each task
112
+ case "task_started": {
113
+ const { description, agent } = wsEvent.data as {
114
+ task_id: string;
115
+ description: string;
116
+ agent: string;
117
+ };
118
+ // Create NEW message for this task (groupId read from ref)
119
+ createNewMessage(`📋 **Task** (${agent})\n\n${description}`);
120
+ break;
121
+ }
122
+
123
+ case "task_completed": {
124
+ // Finalize the task message
125
+ if (currentMessageId) {
126
+ const { output, agent } = wsEvent.data as {
127
+ task_id: string;
128
+ output: string;
129
+ agent: string;
130
+ };
131
+ updateMessage(currentMessageId, (msg) => ({
132
+ ...msg,
133
+ content: `✅ **Task completed** (${agent})\n\n${output}`,
134
+ isStreaming: false,
135
+ }));
136
+ }
137
+ break;
138
+ }
139
+
140
+ // CrewAI tool events - add as tool calls to current message
141
+ case "tool_started": {
142
+ if (currentMessageId) {
143
+ const { tool_name, tool_args, agent } = wsEvent.data as {
144
+ tool_name: string;
145
+ tool_args: string;
146
+ agent: string;
147
+ };
148
+ const toolCall: ToolCall = {
149
+ id: nanoid(),
150
+ name: tool_name,
151
+ args: { input: tool_args, agent },
152
+ status: "running",
153
+ };
154
+ addToolCall(currentMessageId, toolCall);
155
+ }
156
+ break;
157
+ }
158
+
159
+ case "tool_finished": {
160
+ // Tool finished - update last tool call status
161
+ if (currentMessageId) {
162
+ const { tool_name, tool_result } = wsEvent.data as {
163
+ tool_name: string;
164
+ tool_result: string;
165
+ agent: string;
166
+ };
167
+ // Find and update the matching tool call
168
+ updateMessage(currentMessageId, (msg) => {
169
+ const toolCalls = msg.toolCalls || [];
170
+ const lastToolCall = toolCalls.find(
171
+ (tc) => tc.name === tool_name && tc.status === "running"
172
+ );
173
+ if (lastToolCall) {
174
+ return {
175
+ ...msg,
176
+ toolCalls: toolCalls.map((tc) =>
177
+ tc.id === lastToolCall.id
178
+ ? { ...tc, result: tool_result, status: "completed" as const }
179
+ : tc
180
+ ),
181
+ };
182
+ }
183
+ return msg;
184
+ });
185
+ }
186
+ break;
187
+ }
188
+
189
+ // LLM events - silently ignored
190
+ case "llm_started":
191
+ case "llm_completed": {
192
+ break;
193
+ }
194
+
57
195
  case "tool_call": {
58
196
  if (currentMessageId) {
59
197
  const { tool_name, args, tool_call_id } = wsEvent.data as {
@@ -88,21 +226,33 @@ export function useLocalChat() {
88
226
 
89
227
  case "final_result": {
90
228
  if (currentMessageId) {
91
- updateMessage(currentMessageId, (msg) => ({
92
- ...msg,
93
- isStreaming: false,
94
- }));
229
+ const { output } = wsEvent.data as { output: string };
230
+ // For CrewAI, replace content with final output if it exists
231
+ if (output) {
232
+ updateMessage(currentMessageId, (msg) => ({
233
+ ...msg,
234
+ content: msg.content || output,
235
+ isStreaming: false,
236
+ }));
237
+ } else {
238
+ updateMessage(currentMessageId, (msg) => ({
239
+ ...msg,
240
+ isStreaming: false,
241
+ }));
242
+ }
95
243
  }
96
244
  setIsProcessing(false);
97
245
  setCurrentMessageId(null);
246
+ currentGroupIdRef.current = null;
98
247
  break;
99
248
  }
100
249
 
101
250
  case "error": {
102
251
  if (currentMessageId) {
252
+ const { message } = wsEvent.data as { message: string };
103
253
  updateMessage(currentMessageId, (msg) => ({
104
254
  ...msg,
105
- content: msg.content + "\n\n[Error occurred]",
255
+ content: msg.content + `\n\nError: ${message || "Unknown error"}`,
106
256
  isStreaming: false,
107
257
  }));
108
258
  }
@@ -110,6 +260,35 @@ export function useLocalChat() {
110
260
  break;
111
261
  }
112
262
 
263
+ case "tool_approval_required": {
264
+ // Human-in-the-Loop: AI wants to execute tools that need approval
265
+ const { action_requests, review_configs } = wsEvent.data as {
266
+ action_requests: Array<{
267
+ id: string;
268
+ tool_name: string;
269
+ args: Record<string, unknown>;
270
+ }>;
271
+ review_configs: Array<{
272
+ tool_name: string;
273
+ allow_edit?: boolean;
274
+ timeout?: number;
275
+ }>;
276
+ };
277
+ setPendingApproval({
278
+ actionRequests: action_requests,
279
+ reviewConfigs: review_configs,
280
+ });
281
+ // Show pending tools in the current message
282
+ if (currentMessageId) {
283
+ const toolNames = action_requests.map((ar) => ar.tool_name).join(", ");
284
+ updateMessage(currentMessageId, (msg) => ({
285
+ ...msg,
286
+ content: msg.content + `\n\n⏸️ Waiting for approval: ${toolNames}`,
287
+ }));
288
+ }
289
+ break;
290
+ }
291
+
113
292
  case "complete": {
114
293
  setIsProcessing(false);
115
294
  break;
@@ -151,6 +330,49 @@ export function useLocalChat() {
151
330
  createConversation();
152
331
  }, [createConversation]);
153
332
 
333
+ // Human-in-the-Loop: send resume message with user decisions
334
+ const sendResumeDecisions = useCallback(
335
+ (decisions: Decision[]) => {
336
+ // Clear pending approval state
337
+ setPendingApproval(null);
338
+
339
+ // Update message to show decisions were made
340
+ if (currentMessageId) {
341
+ const approvedCount = decisions.filter((d) => d.type === "approve").length;
342
+ const editedCount = decisions.filter((d) => d.type === "edit").length;
343
+ const rejectedCount = decisions.filter((d) => d.type === "reject").length;
344
+
345
+ const summaryParts: string[] = [];
346
+ if (approvedCount > 0) summaryParts.push(`${approvedCount} approved`);
347
+ if (editedCount > 0) summaryParts.push(`${editedCount} edited`);
348
+ if (rejectedCount > 0) summaryParts.push(`${rejectedCount} rejected`);
349
+
350
+ updateMessage(currentMessageId, (msg) => ({
351
+ ...msg,
352
+ content: msg.content.replace(
353
+ /\n\n⏸️ Waiting for approval:.*$/,
354
+ `\n\n✅ Decisions: ${summaryParts.join(", ")}`
355
+ ),
356
+ }));
357
+ }
358
+
359
+ // Send resume message to WebSocket
360
+ sendMessage({
361
+ type: "resume",
362
+ decisions: decisions.map((d) => {
363
+ if (d.type === "edit" && d.editedAction) {
364
+ return {
365
+ type: "edit",
366
+ edited_action: d.editedAction,
367
+ };
368
+ }
369
+ return { type: d.type };
370
+ }),
371
+ });
372
+ },
373
+ [currentMessageId, updateMessage, sendMessage]
374
+ );
375
+
154
376
  return {
155
377
  messages,
156
378
  currentConversationId,
@@ -161,5 +383,8 @@ export function useLocalChat() {
161
383
  sendMessage: sendChatMessage,
162
384
  clearMessages: clearCurrentMessages,
163
385
  startNewChat,
386
+ // Human-in-the-Loop support
387
+ pendingApproval,
388
+ sendResumeDecisions,
164
389
  };
165
390
  }
@@ -36,4 +36,4 @@ export const ROUTES = {
36
36
  } as const;
37
37
 
38
38
  // WebSocket URL (for chat - this needs to be direct to backend for WS)
39
- export const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8000";
39
+ export const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:{{ cookiecutter.backend_port }}";