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
@@ -0,0 +1,563 @@
1
+ {%- if cookiecutter.enable_ai_agent and cookiecutter.use_crewai %}
2
+ """CrewAI Multi-Agent implementation.
3
+
4
+ A multi-agent orchestration framework using CrewAI.
5
+ Enables teams of AI agents to work together on complex tasks.
6
+ Uses CrewAI's event system for real-time streaming to WebSocket.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ from queue import Empty, Queue
13
+ from threading import Thread
14
+ from typing import Any, TypedDict
15
+
16
+ # Disable CrewAI interactive prompts for server use
17
+ os.environ.setdefault("CREWAI_DISABLE_TRACES_PROMPT", "true")
18
+
19
+ from crewai import Agent, Crew, Process, Task
20
+ from crewai.events import (
21
+ crewai_event_bus,
22
+ CrewKickoffStartedEvent,
23
+ CrewKickoffCompletedEvent,
24
+ CrewKickoffFailedEvent,
25
+ AgentExecutionStartedEvent,
26
+ AgentExecutionCompletedEvent,
27
+ TaskStartedEvent,
28
+ TaskCompletedEvent,
29
+ ToolUsageStartedEvent,
30
+ ToolUsageFinishedEvent,
31
+ LLMCallStartedEvent,
32
+ LLMCallCompletedEvent,
33
+ )
34
+ from pydantic import BaseModel, Field
35
+ {%- if cookiecutter.use_openai %}
36
+ from langchain_openai import ChatOpenAI
37
+ {%- endif %}
38
+ {%- if cookiecutter.use_anthropic %}
39
+ from langchain_anthropic import ChatAnthropic
40
+ {%- endif %}
41
+
42
+ from app.agents.prompts import DEFAULT_SYSTEM_PROMPT
43
+ from app.core.config import settings
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class AgentConfig(BaseModel):
49
+ """Configuration for a single agent."""
50
+
51
+ role: str = Field(..., description="Agent's role/title")
52
+ goal: str = Field(..., description="Agent's primary goal")
53
+ backstory: str = Field(..., description="Agent's background context")
54
+ tools: list[str] = Field(default_factory=list)
55
+ allow_delegation: bool = True
56
+ verbose: bool = True
57
+
58
+
59
+ class TaskConfig(BaseModel):
60
+ """Configuration for a single task."""
61
+
62
+ description: str = Field(..., description="Task description")
63
+ expected_output: str = Field(..., description="Expected output format")
64
+ agent_role: str = Field(..., description="Role of agent to execute this")
65
+ context_from: list[str] = Field(default_factory=list)
66
+
67
+
68
+ class CrewConfig(BaseModel):
69
+ """Configuration for the entire crew."""
70
+
71
+ name: str = "default_crew"
72
+ process: str = "sequential" # sequential, hierarchical
73
+ memory: bool = True
74
+ max_rpm: int = 10
75
+ agents: list[AgentConfig] = Field(default_factory=list)
76
+ tasks: list[TaskConfig] = Field(default_factory=list)
77
+
78
+
79
+ class CrewContext(TypedDict, total=False):
80
+ """Runtime context for crew execution."""
81
+
82
+ user_id: str | None
83
+ user_name: str | None
84
+ metadata: dict[str, Any]
85
+
86
+
87
+ class CrewEventQueueListener:
88
+ """Event listener that sends CrewAI events to a queue for WebSocket streaming.
89
+
90
+ Registers handlers with the global crewai_event_bus to capture all events
91
+ and forward them to a queue for async WebSocket streaming.
92
+ """
93
+
94
+ def __init__(self, event_queue: Queue):
95
+ self._event_queue = event_queue
96
+ self._handlers: list[Any] = []
97
+ self._register_handlers()
98
+
99
+ def _register_handlers(self):
100
+ """Register all event handlers with the CrewAI event bus."""
101
+
102
+ def on_crew_started(source, event: CrewKickoffStartedEvent):
103
+ self._event_queue.put({
104
+ "type": "crew_started",
105
+ "crew_name": getattr(event, "crew_name", "crew"),
106
+ "crew_id": str(getattr(event, "crew_id", "")),
107
+ })
108
+
109
+ def on_crew_completed(source, event: CrewKickoffCompletedEvent):
110
+ output = getattr(event, "output", None)
111
+ self._event_queue.put({
112
+ "type": "crew_complete",
113
+ "result": str(output.raw if hasattr(output, "raw") else output) if output else "",
114
+ })
115
+
116
+ def on_crew_failed(source, event: CrewKickoffFailedEvent):
117
+ self._event_queue.put({
118
+ "type": "error",
119
+ "error": str(getattr(event, "error", "Unknown error")),
120
+ })
121
+
122
+ def on_agent_started(source, event: AgentExecutionStartedEvent):
123
+ agent = getattr(event, "agent", None)
124
+ self._event_queue.put({
125
+ "type": "agent_started",
126
+ "agent": getattr(agent, "role", "Unknown") if agent else "Unknown",
127
+ "task": str(getattr(event, "task", "")),
128
+ })
129
+
130
+ def on_agent_completed(source, event: AgentExecutionCompletedEvent):
131
+ agent = getattr(event, "agent", None)
132
+ output = getattr(event, "output", None)
133
+ self._event_queue.put({
134
+ "type": "agent_completed",
135
+ "agent": getattr(agent, "role", "Unknown") if agent else "Unknown",
136
+ "output": str(output) if output else "",
137
+ })
138
+
139
+ def on_task_started(source, event: TaskStartedEvent):
140
+ task = getattr(event, "task", None)
141
+ self._event_queue.put({
142
+ "type": "task_started",
143
+ "task_id": str(getattr(task, "id", "")) if task else "",
144
+ "description": str(getattr(task, "description", "")) if task else "",
145
+ "agent": getattr(getattr(task, "agent", None), "role", "Unknown") if task else "Unknown",
146
+ })
147
+
148
+ def on_task_completed(source, event: TaskCompletedEvent):
149
+ task = getattr(event, "task", None)
150
+ output = getattr(event, "output", None)
151
+ self._event_queue.put({
152
+ "type": "task_completed",
153
+ "task_id": str(getattr(task, "id", "")) if task else "",
154
+ "output": str(output.raw if hasattr(output, "raw") else output) if output else "",
155
+ "agent": getattr(getattr(task, "agent", None), "role", "Unknown") if task else "Unknown",
156
+ })
157
+
158
+ def on_tool_started(source, event: ToolUsageStartedEvent):
159
+ self._event_queue.put({
160
+ "type": "tool_started",
161
+ "tool_name": str(getattr(event, "tool_name", "Unknown")),
162
+ "tool_args": str(getattr(event, "tool_args", {})),
163
+ "agent": str(getattr(event, "agent", "Unknown")),
164
+ })
165
+
166
+ def on_tool_finished(source, event: ToolUsageFinishedEvent):
167
+ self._event_queue.put({
168
+ "type": "tool_finished",
169
+ "tool_name": str(getattr(event, "tool_name", "Unknown")),
170
+ "tool_result": str(getattr(event, "tool_result", "")),
171
+ "agent": str(getattr(event, "agent", "Unknown")),
172
+ })
173
+
174
+ def on_llm_started(source, event: LLMCallStartedEvent):
175
+ self._event_queue.put({
176
+ "type": "llm_started",
177
+ "agent": str(getattr(event, "agent", "Unknown")),
178
+ })
179
+
180
+ def on_llm_completed(source, event: LLMCallCompletedEvent):
181
+ response = getattr(event, "response", None)
182
+ self._event_queue.put({
183
+ "type": "llm_completed",
184
+ "agent": str(getattr(event, "agent", "Unknown")),
185
+ "response": str(response) if response else "",
186
+ })
187
+
188
+ # Register handlers with the event bus
189
+ crewai_event_bus.on(CrewKickoffStartedEvent)(on_crew_started)
190
+ crewai_event_bus.on(CrewKickoffCompletedEvent)(on_crew_completed)
191
+ crewai_event_bus.on(CrewKickoffFailedEvent)(on_crew_failed)
192
+ crewai_event_bus.on(AgentExecutionStartedEvent)(on_agent_started)
193
+ crewai_event_bus.on(AgentExecutionCompletedEvent)(on_agent_completed)
194
+ crewai_event_bus.on(TaskStartedEvent)(on_task_started)
195
+ crewai_event_bus.on(TaskCompletedEvent)(on_task_completed)
196
+ crewai_event_bus.on(ToolUsageStartedEvent)(on_tool_started)
197
+ crewai_event_bus.on(ToolUsageFinishedEvent)(on_tool_finished)
198
+ crewai_event_bus.on(LLMCallStartedEvent)(on_llm_started)
199
+ crewai_event_bus.on(LLMCallCompletedEvent)(on_llm_completed)
200
+
201
+ # Store references to prevent garbage collection
202
+ self._handlers = [
203
+ on_crew_started, on_crew_completed, on_crew_failed,
204
+ on_agent_started, on_agent_completed,
205
+ on_task_started, on_task_completed,
206
+ on_tool_started, on_tool_finished,
207
+ on_llm_started, on_llm_completed,
208
+ ]
209
+
210
+
211
+ class CrewAIAssistant:
212
+ """Multi-agent crew orchestration using CrewAI.
213
+
214
+ Supports:
215
+ - Multiple specialized agents with different roles
216
+ - Sequential or hierarchical task execution
217
+ - Agent delegation and collaboration
218
+ - Real-time event streaming via WebSocket
219
+
220
+ CrewAI Pattern:
221
+ 1. Define agents with roles, goals, and backstories
222
+ 2. Define tasks assigned to specific agents
223
+ 3. Crew executes tasks in order (sequential/hierarchical)
224
+ 4. Events are streamed in real-time to connected clients
225
+ 5. Final output aggregated from all task results
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ config: CrewConfig | None = None,
231
+ model_name: str | None = None,
232
+ temperature: float | None = None,
233
+ system_prompt: str | None = None,
234
+ ):
235
+ self.config = config or self._default_config()
236
+ self.model_name = model_name or settings.AI_MODEL
237
+ self.temperature = temperature or settings.AI_TEMPERATURE
238
+ self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
239
+ self._crew: Crew | None = None
240
+ self._agents: dict[str, Agent] = {}
241
+
242
+ def _default_config(self) -> CrewConfig:
243
+ """Default crew configuration for general assistance."""
244
+ return CrewConfig(
245
+ name="assistant_crew",
246
+ process="sequential",
247
+ memory=False, # Disable memory for simpler setup
248
+ agents=[
249
+ AgentConfig(
250
+ role="Research Analyst",
251
+ goal="Gather and analyze information accurately to help the user",
252
+ backstory="You are an expert research analyst skilled at finding and synthesizing information. You always provide accurate, well-researched answers.",
253
+ tools=[],
254
+ allow_delegation=False, # Simpler without delegation
255
+ ),
256
+ AgentConfig(
257
+ role="Content Writer",
258
+ goal="Create clear, well-structured responses for the user",
259
+ backstory="You are a skilled writer who produces high-quality, readable content. You take research findings and transform them into helpful responses.",
260
+ tools=[],
261
+ allow_delegation=False,
262
+ ),
263
+ ],
264
+ tasks=[
265
+ TaskConfig(
266
+ description="Research and analyze the user's query: {user_input}. Gather all relevant information needed to provide a comprehensive answer.",
267
+ expected_output="Comprehensive research findings with key facts and insights",
268
+ agent_role="Research Analyst",
269
+ ),
270
+ TaskConfig(
271
+ description="Based on the research findings, write a clear, helpful response to the user's original query.",
272
+ expected_output="A well-written, user-friendly response that addresses the query",
273
+ agent_role="Content Writer",
274
+ context_from=["Research Analyst"],
275
+ ),
276
+ ],
277
+ )
278
+
279
+ def _get_llm(self):
280
+ """Get LLM instance based on settings."""
281
+ {%- if cookiecutter.use_openai %}
282
+ return ChatOpenAI(
283
+ model=self.model_name,
284
+ temperature=self.temperature,
285
+ api_key=settings.OPENAI_API_KEY,
286
+ )
287
+ {%- endif %}
288
+ {%- if cookiecutter.use_anthropic %}
289
+ return ChatAnthropic(
290
+ model=self.model_name,
291
+ temperature=self.temperature,
292
+ api_key=settings.ANTHROPIC_API_KEY,
293
+ )
294
+ {%- endif %}
295
+
296
+ def _build_agents(self) -> dict[str, Agent]:
297
+ """Build Agent instances from config."""
298
+ agents = {}
299
+ llm = self._get_llm()
300
+
301
+ for agent_config in self.config.agents:
302
+ agent = Agent(
303
+ role=agent_config.role,
304
+ goal=agent_config.goal,
305
+ backstory=agent_config.backstory,
306
+ tools=[],
307
+ allow_delegation=agent_config.allow_delegation,
308
+ verbose=agent_config.verbose,
309
+ llm=llm,
310
+ )
311
+ agents[agent_config.role] = agent
312
+
313
+ return agents
314
+
315
+ def _build_tasks(self, agents: dict[str, Agent]) -> list[Task]:
316
+ """Build Task instances from config."""
317
+ tasks = []
318
+ task_by_agent: dict[str, Task] = {}
319
+
320
+ for task_config in self.config.tasks:
321
+ agent = agents.get(task_config.agent_role)
322
+ if not agent:
323
+ raise ValueError(f"Agent '{task_config.agent_role}' not found")
324
+
325
+ context = [
326
+ task_by_agent[role]
327
+ for role in task_config.context_from
328
+ if role in task_by_agent
329
+ ]
330
+
331
+ task = Task(
332
+ description=task_config.description,
333
+ expected_output=task_config.expected_output,
334
+ agent=agent,
335
+ context=context if context else None,
336
+ )
337
+ tasks.append(task)
338
+ task_by_agent[task_config.agent_role] = task
339
+
340
+ return tasks
341
+
342
+ def _build_crew(self) -> Crew:
343
+ """Build and return the Crew instance."""
344
+ self._agents = self._build_agents()
345
+ tasks = self._build_tasks(self._agents)
346
+
347
+ process = (
348
+ Process.hierarchical
349
+ if self.config.process == "hierarchical"
350
+ else Process.sequential
351
+ )
352
+
353
+ return Crew(
354
+ agents=list(self._agents.values()),
355
+ tasks=tasks,
356
+ process=process,
357
+ memory=self.config.memory,
358
+ verbose=False, # Disable console output for server use
359
+ )
360
+
361
+ @property
362
+ def crew(self) -> Crew:
363
+ """Get or create the Crew instance."""
364
+ if self._crew is None:
365
+ self._crew = self._build_crew()
366
+ return self._crew
367
+
368
+ async def run(
369
+ self,
370
+ user_input: str,
371
+ history: list[dict[str, str]] | None = None,
372
+ context: CrewContext | None = None,
373
+ thread_id: str = "default",
374
+ ) -> tuple[str, list[dict[str, Any]], CrewContext]:
375
+ """Run the crew and return results.
376
+
377
+ Args:
378
+ user_input: User's message/request.
379
+ history: Conversation history (for context).
380
+ context: Runtime context.
381
+ thread_id: Thread ID for conversation continuity.
382
+
383
+ Returns:
384
+ Tuple of (output_text, task_results, context).
385
+ """
386
+ crew_context: CrewContext = context if context is not None else {}
387
+
388
+ inputs = {
389
+ "user_input": user_input,
390
+ "history": self._format_history(history),
391
+ **crew_context.get("metadata", {}),
392
+ }
393
+
394
+ logger.info(f"Starting CrewAI execution: {user_input[:100]}...")
395
+
396
+ # Reset crew for fresh execution
397
+ self._crew = None
398
+
399
+ loop = asyncio.get_event_loop()
400
+ result = await loop.run_in_executor(
401
+ None,
402
+ lambda: self.crew.kickoff(inputs=inputs)
403
+ )
404
+
405
+ task_results = []
406
+ for task in self.crew.tasks:
407
+ if task.output:
408
+ task_results.append({
409
+ "agent": task.agent.role if task.agent else "Unknown",
410
+ "description": task.description[:100],
411
+ "output": str(task.output.raw if hasattr(task.output, "raw") else task.output),
412
+ })
413
+
414
+ output = str(result.raw if hasattr(result, "raw") else result) if result else ""
415
+ logger.info(f"CrewAI execution complete. Output length: {len(output)}")
416
+
417
+ return output, task_results, crew_context
418
+
419
+ async def stream(
420
+ self,
421
+ user_input: str,
422
+ history: list[dict[str, str]] | None = None,
423
+ context: CrewContext | None = None,
424
+ thread_id: str = "default",
425
+ ):
426
+ """Stream crew execution with real-time event updates.
427
+
428
+ Uses CrewAI's event system to capture and stream:
429
+ - crew_started: Crew execution begins
430
+ - agent_started/completed: Agent lifecycle events
431
+ - task_started/completed: Task lifecycle events
432
+ - tool_started/finished: Tool usage events
433
+ - llm_started/completed: LLM call events
434
+ - crew_complete: Final result
435
+ - error: Error occurred
436
+
437
+ Args:
438
+ user_input: User's message.
439
+ history: Conversation history.
440
+ context: Optional runtime context.
441
+ thread_id: Thread ID for conversation continuity.
442
+
443
+ Yields:
444
+ Dict events with type and data.
445
+ """
446
+ event_queue: Queue = Queue()
447
+
448
+ inputs = {
449
+ "user_input": user_input,
450
+ "history": self._format_history(history),
451
+ }
452
+
453
+ # Reset crew for fresh execution
454
+ self._crew = None
455
+
456
+ # Create event listener BEFORE starting thread (keeps reference alive)
457
+ listener = CrewEventQueueListener(event_queue)
458
+
459
+ def run_with_events():
460
+ """Run crew with event listener."""
461
+ nonlocal listener # Keep reference to prevent GC
462
+ try:
463
+ # Build and run crew
464
+ crew = self.crew
465
+ result = crew.kickoff(inputs=inputs)
466
+
467
+ # Ensure final result is sent (event bus may have already sent it)
468
+ if result:
469
+ event_queue.put({
470
+ "type": "crew_complete",
471
+ "result": str(result.raw if hasattr(result, "raw") else result),
472
+ })
473
+
474
+ except Exception as e:
475
+ logger.error(f"CrewAI execution error: {e}", exc_info=True)
476
+ event_queue.put({
477
+ "type": "error",
478
+ "error": str(e),
479
+ })
480
+ finally:
481
+ event_queue.put(None) # Signal completion
482
+
483
+ # Start crew in background thread
484
+ thread = Thread(target=run_with_events, daemon=True)
485
+ thread.start()
486
+
487
+ # Yield events as they arrive
488
+ while True:
489
+ await asyncio.sleep(0.05)
490
+
491
+ while True:
492
+ try:
493
+ event = event_queue.get_nowait()
494
+ if event is None:
495
+ thread.join(timeout=2.0)
496
+ _ = listener # Keep reference until done
497
+ return
498
+ yield event
499
+ except Empty:
500
+ break
501
+
502
+ # Check if thread is still alive
503
+ if not thread.is_alive():
504
+ # Drain remaining events
505
+ while True:
506
+ try:
507
+ event = event_queue.get_nowait()
508
+ if event is None:
509
+ _ = listener # Keep reference until done
510
+ return
511
+ yield event
512
+ except Empty:
513
+ break
514
+ _ = listener # Keep reference until done
515
+ return
516
+
517
+ def _format_history(self, history: list[dict[str, str]] | None) -> str:
518
+ """Format conversation history as context string."""
519
+ if not history:
520
+ return ""
521
+
522
+ formatted = []
523
+ for msg in history[-5:]:
524
+ role = msg.get("role", "unknown")
525
+ content = msg.get("content", "")
526
+ formatted.append(f"{role.upper()}: {content}")
527
+
528
+ return "\n".join(formatted)
529
+
530
+
531
+ def get_crew() -> CrewAIAssistant:
532
+ """Factory function to create a CrewAIAssistant.
533
+
534
+ Returns:
535
+ Configured CrewAIAssistant instance.
536
+ """
537
+ return CrewAIAssistant()
538
+
539
+
540
+ async def run_crew(
541
+ user_input: str,
542
+ history: list[dict[str, str]],
543
+ context: CrewContext | None = None,
544
+ thread_id: str = "default",
545
+ ) -> tuple[str, list[dict[str, Any]], CrewContext]:
546
+ """Run crew and return the output along with task results.
547
+
548
+ This is a convenience function for backwards compatibility.
549
+
550
+ Args:
551
+ user_input: User's message.
552
+ history: Conversation history.
553
+ context: Optional runtime context.
554
+ thread_id: Thread ID for conversation continuity.
555
+
556
+ Returns:
557
+ Tuple of (output_text, task_results, context).
558
+ """
559
+ crew = get_crew()
560
+ return await crew.run(user_input, history, context, thread_id)
561
+ {%- else %}
562
+ """CrewAI Assistant agent - not configured."""
563
+ {%- endif %}