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.
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
- fastapi_gen/__init__.py +6 -1
- fastapi_gen/cli.py +9 -0
- fastapi_gen/config.py +154 -2
- fastapi_gen/generator.py +34 -14
- fastapi_gen/prompts.py +172 -31
- fastapi_gen/template/VARIABLES.md +33 -4
- fastapi_gen/template/cookiecutter.json +10 -0
- fastapi_gen/template/hooks/post_gen_project.py +87 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
- {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 %}
|