swarmforge 0.1.0__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.
- swarmforge/__init__.py +5 -0
- swarmforge/api/__init__.py +5 -0
- swarmforge/api/fastapi.py +485 -0
- swarmforge/authoring/__init__.py +25 -0
- swarmforge/authoring/prompts.py +165 -0
- swarmforge/authoring/validators.py +276 -0
- swarmforge/evaluation/__init__.py +33 -0
- swarmforge/evaluation/provider/__init__.py +6 -0
- swarmforge/evaluation/provider/client.py +60 -0
- swarmforge/evaluation/provider/config.py +176 -0
- swarmforge/evaluation/runner/__init__.py +7 -0
- swarmforge/evaluation/runner/conversation.py +396 -0
- swarmforge/evaluation/runner/tool_mocks.py +104 -0
- swarmforge/evaluation/runner/user_simulator.py +123 -0
- swarmforge/evaluation/swarm.py +927 -0
- swarmforge/evaluation/trace/__init__.py +5 -0
- swarmforge/evaluation/trace/capture.py +81 -0
- swarmforge/swarm/__init__.py +47 -0
- swarmforge/swarm/models.py +272 -0
- swarmforge/swarm/orchestrator.py +1268 -0
- swarmforge/swarm/stores/__init__.py +6 -0
- swarmforge/swarm/stores/base.py +28 -0
- swarmforge/swarm/stores/memory.py +34 -0
- swarmforge/swarm/tools.py +432 -0
- swarmforge-0.1.0.dist-info/METADATA +815 -0
- swarmforge-0.1.0.dist-info/RECORD +29 -0
- swarmforge-0.1.0.dist-info/WHEEL +5 -0
- swarmforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- swarmforge-0.1.0.dist-info/top_level.txt +1 -0
swarmforge/__init__.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""FastAPI integration for exposing the swarm runtime over HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, AsyncGenerator, Callable, Dict, List
|
|
8
|
+
|
|
9
|
+
from fastapi import Body, FastAPI, HTTPException, Response
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from ..authoring import build_swarm_definition
|
|
15
|
+
from ..evaluation.provider import ModelConfig, OpenAIClientWrapper
|
|
16
|
+
from ..evaluation.swarm import serialize_checkpoints
|
|
17
|
+
from ..swarm import InMemorySessionStore, SessionStore, SwarmSession, process_swarm_stream
|
|
18
|
+
from ..swarm.orchestrator import AgentTurnConfig, StreamAgentIteration
|
|
19
|
+
from ..swarm.tools import FrameworkToolRegistry, ToolRegistry
|
|
20
|
+
|
|
21
|
+
RUN_SWARM_OPENAPI_EXAMPLES = {
|
|
22
|
+
"openrouter": {
|
|
23
|
+
"summary": "Run a swarm turn with OpenRouter",
|
|
24
|
+
"value": {
|
|
25
|
+
"swarm": {
|
|
26
|
+
"id": "support",
|
|
27
|
+
"name": "Support Swarm",
|
|
28
|
+
"nodes": [
|
|
29
|
+
{
|
|
30
|
+
"node_key": "assistant",
|
|
31
|
+
"name": "Assistant",
|
|
32
|
+
"system_prompt": "You are a helpful assistant.",
|
|
33
|
+
"intent": "Handle the full request",
|
|
34
|
+
"capabilities": ["Answer questions"],
|
|
35
|
+
"is_entry_node": True,
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"edges": [],
|
|
39
|
+
"variables": [],
|
|
40
|
+
},
|
|
41
|
+
"user_input": "Give me a concise answer.",
|
|
42
|
+
"provider": {
|
|
43
|
+
"provider": "openrouter",
|
|
44
|
+
"model": "openrouter/auto",
|
|
45
|
+
"site_url": "https://your-app.example",
|
|
46
|
+
"app_name": "Your App Name",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
"gemini": {
|
|
51
|
+
"summary": "Run a swarm turn with Gemini OpenAI-compat",
|
|
52
|
+
"value": {
|
|
53
|
+
"swarm": {
|
|
54
|
+
"id": "support",
|
|
55
|
+
"name": "Support Swarm",
|
|
56
|
+
"nodes": [
|
|
57
|
+
{
|
|
58
|
+
"node_key": "assistant",
|
|
59
|
+
"name": "Assistant",
|
|
60
|
+
"system_prompt": "You are a helpful assistant.",
|
|
61
|
+
"intent": "Handle the full request",
|
|
62
|
+
"capabilities": ["Answer questions"],
|
|
63
|
+
"is_entry_node": True,
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"edges": [],
|
|
67
|
+
"variables": [],
|
|
68
|
+
},
|
|
69
|
+
"user_input": "Explain the answer briefly.",
|
|
70
|
+
"provider": {
|
|
71
|
+
"provider": "gemini",
|
|
72
|
+
"model": "gemini-3-flash-preview",
|
|
73
|
+
"default_chat_params": {"reasoning_effort": "low"},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
SEND_MESSAGE_OPENAPI_EXAMPLES = {
|
|
80
|
+
"openrouter": {
|
|
81
|
+
"summary": "Send a session message with OpenRouter",
|
|
82
|
+
"value": {
|
|
83
|
+
"user_input": "Help me with this request.",
|
|
84
|
+
"provider": {
|
|
85
|
+
"provider": "openrouter",
|
|
86
|
+
"model": "openrouter/auto",
|
|
87
|
+
"site_url": "https://your-app.example",
|
|
88
|
+
"app_name": "Your App Name",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
"gemini": {
|
|
93
|
+
"summary": "Send a session message with Gemini OpenAI-compat",
|
|
94
|
+
"value": {
|
|
95
|
+
"user_input": "Help me with this request.",
|
|
96
|
+
"provider": {
|
|
97
|
+
"provider": "gemini",
|
|
98
|
+
"model": "gemini-3-flash-preview",
|
|
99
|
+
"default_chat_params": {"reasoning_effort": "low"},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
CREATE_SESSION_OPENAPI_EXAMPLE = {
|
|
106
|
+
"summary": "Create a swarm session",
|
|
107
|
+
"value": {
|
|
108
|
+
"session_id": "session-1",
|
|
109
|
+
"swarm": {
|
|
110
|
+
"id": "single-agent",
|
|
111
|
+
"name": "Single Agent",
|
|
112
|
+
"nodes": [
|
|
113
|
+
{
|
|
114
|
+
"node_key": "assistant",
|
|
115
|
+
"name": "Assistant",
|
|
116
|
+
"system_prompt": "You are a helpful assistant.",
|
|
117
|
+
"intent": "Handle the full request",
|
|
118
|
+
"capabilities": ["Answer questions"],
|
|
119
|
+
"is_entry_node": True,
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"edges": [],
|
|
123
|
+
"variables": [],
|
|
124
|
+
},
|
|
125
|
+
"global_variables": {},
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ProviderConfigPayload(BaseModel):
|
|
131
|
+
provider: str | None = None
|
|
132
|
+
base_url: str | None = None
|
|
133
|
+
api_key: str | None = None
|
|
134
|
+
model: str | None = None
|
|
135
|
+
temperature: float | None = None
|
|
136
|
+
max_tokens: int | None = None
|
|
137
|
+
site_url: str | None = None
|
|
138
|
+
app_name: str | None = None
|
|
139
|
+
default_headers: Dict[str, str] | None = None
|
|
140
|
+
default_chat_params: Dict[str, Any] | None = None
|
|
141
|
+
|
|
142
|
+
def to_model_config(self, base: ModelConfig | None = None) -> ModelConfig:
|
|
143
|
+
base = base or ModelConfig()
|
|
144
|
+
provider = self.provider or base.provider
|
|
145
|
+
same_provider = provider == base.provider
|
|
146
|
+
return ModelConfig(
|
|
147
|
+
provider=provider,
|
|
148
|
+
base_url=self.base_url if self.base_url is not None else (base.base_url if same_provider else None),
|
|
149
|
+
api_key=self.api_key if self.api_key is not None else (base.api_key if same_provider else None),
|
|
150
|
+
model=self.model or base.model,
|
|
151
|
+
temperature=self.temperature if self.temperature is not None else base.temperature,
|
|
152
|
+
max_tokens=self.max_tokens if self.max_tokens is not None else base.max_tokens,
|
|
153
|
+
site_url=self.site_url,
|
|
154
|
+
app_name=self.app_name,
|
|
155
|
+
default_headers=self.default_headers if self.default_headers is not None else (base.default_headers if same_provider else None),
|
|
156
|
+
default_chat_params=self.default_chat_params if self.default_chat_params is not None else (base.default_chat_params if same_provider else None),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SwarmVariablePayload(BaseModel):
|
|
161
|
+
key_name: str
|
|
162
|
+
description: str = ""
|
|
163
|
+
reducer_rule: str = "overwrite"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SwarmEdgePayload(BaseModel):
|
|
167
|
+
source_node_key: str
|
|
168
|
+
target_node_key: str
|
|
169
|
+
handoff_description: str
|
|
170
|
+
required_variables: List[str] = Field(default_factory=list)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SwarmNodePayload(BaseModel):
|
|
174
|
+
node_key: str
|
|
175
|
+
name: str
|
|
176
|
+
system_prompt: str = ""
|
|
177
|
+
intent: str = ""
|
|
178
|
+
capabilities: List[str] = Field(default_factory=list)
|
|
179
|
+
persona: str = ""
|
|
180
|
+
model_choice: str = ""
|
|
181
|
+
enabled_tools: List[Dict[str, Any]] = Field(default_factory=list)
|
|
182
|
+
behavior_config: Dict[str, Any] = Field(default_factory=dict)
|
|
183
|
+
is_entry_node: bool = False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class SwarmDefinitionPayload(BaseModel):
|
|
187
|
+
id: str = "swarm"
|
|
188
|
+
name: str = "Swarm"
|
|
189
|
+
graph_version: int = 1
|
|
190
|
+
nodes: List[SwarmNodePayload]
|
|
191
|
+
edges: List[SwarmEdgePayload] = Field(default_factory=list)
|
|
192
|
+
variables: List[SwarmVariablePayload] = Field(default_factory=list)
|
|
193
|
+
|
|
194
|
+
def to_runtime(self):
|
|
195
|
+
return build_swarm_definition(
|
|
196
|
+
{
|
|
197
|
+
"nodes": [node.model_dump(mode="python") for node in self.nodes],
|
|
198
|
+
"edges": [edge.model_dump(mode="python") for edge in self.edges],
|
|
199
|
+
"variables": [variable.model_dump(mode="python") for variable in self.variables],
|
|
200
|
+
},
|
|
201
|
+
swarm_id=self.id,
|
|
202
|
+
name=self.name,
|
|
203
|
+
graph_version=self.graph_version,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CreateSessionRequest(BaseModel):
|
|
208
|
+
session_id: str | None = None
|
|
209
|
+
swarm: SwarmDefinitionPayload
|
|
210
|
+
global_variables: Dict[str, Any] = Field(default_factory=dict)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SendMessageRequest(BaseModel):
|
|
214
|
+
user_input: str
|
|
215
|
+
provider: ProviderConfigPayload | None = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class RunSwarmRequest(BaseModel):
|
|
219
|
+
session_id: str | None = None
|
|
220
|
+
swarm: SwarmDefinitionPayload
|
|
221
|
+
user_input: str
|
|
222
|
+
global_variables: Dict[str, Any] = Field(default_factory=dict)
|
|
223
|
+
provider: ProviderConfigPayload | None = None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _session_payload(session: SwarmSession) -> Dict[str, Any]:
|
|
227
|
+
return {
|
|
228
|
+
"id": session.id,
|
|
229
|
+
"swarm_id": session.swarm.id,
|
|
230
|
+
"swarm_name": session.swarm.name,
|
|
231
|
+
"graph_version": session.swarm.graph_version,
|
|
232
|
+
"current_node_id": session.current_node_id,
|
|
233
|
+
"current_node_key": session.current_node.node_key if session.current_node else None,
|
|
234
|
+
"current_agent": session.current_node.name if session.current_node else None,
|
|
235
|
+
"snapshot": session.snapshot(),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _sse_event(event: str, payload: Dict[str, Any]) -> str:
|
|
240
|
+
return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_openai_compatible_stream_agent_iteration(client: OpenAIClientWrapper) -> StreamAgentIteration:
|
|
244
|
+
async def _stream_agent_iteration(
|
|
245
|
+
*,
|
|
246
|
+
agent_node,
|
|
247
|
+
contents,
|
|
248
|
+
config: AgentTurnConfig,
|
|
249
|
+
) -> AsyncGenerator[tuple[str, List[Dict[str, Any]], Any], None]:
|
|
250
|
+
del agent_node
|
|
251
|
+
messages: List[Dict[str, Any]] = []
|
|
252
|
+
if config.system_instruction:
|
|
253
|
+
messages.append({"role": "system", "content": config.system_instruction})
|
|
254
|
+
|
|
255
|
+
for item in contents:
|
|
256
|
+
role = str(item.get("role") or "user").strip()
|
|
257
|
+
if role == "model":
|
|
258
|
+
role = "assistant"
|
|
259
|
+
message: Dict[str, Any] = {"role": role, "content": item.get("content", "")}
|
|
260
|
+
if role == "tool":
|
|
261
|
+
if item.get("tool_call_id"):
|
|
262
|
+
message["tool_call_id"] = item.get("tool_call_id")
|
|
263
|
+
if item.get("name"):
|
|
264
|
+
message["name"] = item.get("name")
|
|
265
|
+
messages.append(message)
|
|
266
|
+
|
|
267
|
+
response = client.chat_completion(messages=messages, tools=config.tools)
|
|
268
|
+
assistant_message = response.choices[0].message
|
|
269
|
+
tool_calls: List[Dict[str, Any]] = []
|
|
270
|
+
if assistant_message.tool_calls:
|
|
271
|
+
for tool_call in assistant_message.tool_calls:
|
|
272
|
+
args = tool_call.function.arguments
|
|
273
|
+
if not isinstance(args, dict):
|
|
274
|
+
args = json.loads(args or "{}")
|
|
275
|
+
tool_calls.append(
|
|
276
|
+
{
|
|
277
|
+
"id": tool_call.id,
|
|
278
|
+
"name": tool_call.function.name,
|
|
279
|
+
"args": args,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
yield assistant_message.content or "", tool_calls, response
|
|
283
|
+
|
|
284
|
+
return _stream_agent_iteration
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class _SwarmApiRuntime:
|
|
288
|
+
def __init__(self, store: SessionStore) -> None:
|
|
289
|
+
self.store = store
|
|
290
|
+
|
|
291
|
+
async def create_session(self, *, swarm, session_id: str | None, global_variables: Dict[str, Any]) -> SwarmSession:
|
|
292
|
+
resolved_session_id = session_id or uuid.uuid4().hex
|
|
293
|
+
existing = await self.store.get_session(resolved_session_id)
|
|
294
|
+
if existing is not None:
|
|
295
|
+
raise ValueError(f"Session '{resolved_session_id}' already exists")
|
|
296
|
+
session = SwarmSession(
|
|
297
|
+
id=resolved_session_id,
|
|
298
|
+
swarm=swarm,
|
|
299
|
+
global_variables=dict(global_variables or {}),
|
|
300
|
+
)
|
|
301
|
+
await self.store.save_session(session)
|
|
302
|
+
return session
|
|
303
|
+
|
|
304
|
+
async def get_session(self, session_id: str) -> SwarmSession:
|
|
305
|
+
session = await self.store.get_session(session_id)
|
|
306
|
+
if session is None:
|
|
307
|
+
raise KeyError(session_id)
|
|
308
|
+
return session
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def create_fastapi_app(
|
|
312
|
+
*,
|
|
313
|
+
default_model_config: ModelConfig | None = None,
|
|
314
|
+
session_store: SessionStore | None = None,
|
|
315
|
+
stream_agent_iteration_factory: Callable[[ModelConfig], StreamAgentIteration] | None = None,
|
|
316
|
+
tool_registry: ToolRegistry | FrameworkToolRegistry | None = None,
|
|
317
|
+
tool_state: Any = None,
|
|
318
|
+
cors_allow_origins: List[str] | None = None,
|
|
319
|
+
) -> FastAPI:
|
|
320
|
+
resolved_store = session_store or InMemorySessionStore()
|
|
321
|
+
app = FastAPI(title="SwarmForge API", version="0.1.0")
|
|
322
|
+
app.add_middleware(
|
|
323
|
+
CORSMiddleware,
|
|
324
|
+
allow_origins=cors_allow_origins or ["*"],
|
|
325
|
+
allow_credentials=False,
|
|
326
|
+
allow_methods=["*"],
|
|
327
|
+
allow_headers=["*"],
|
|
328
|
+
)
|
|
329
|
+
app.state.runtime = _SwarmApiRuntime(resolved_store)
|
|
330
|
+
app.state.default_model_config = default_model_config
|
|
331
|
+
app.state.tool_registry = tool_registry
|
|
332
|
+
app.state.tool_state = tool_state
|
|
333
|
+
app.state.stream_agent_iteration_factory = stream_agent_iteration_factory or (
|
|
334
|
+
lambda config: build_openai_compatible_stream_agent_iteration(OpenAIClientWrapper(config))
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def _resolve_config(provider: ProviderConfigPayload | None) -> ModelConfig:
|
|
338
|
+
base_config = app.state.default_model_config
|
|
339
|
+
return provider.to_model_config(base_config) if provider is not None else (base_config or ModelConfig())
|
|
340
|
+
|
|
341
|
+
async def _run_turn(session: SwarmSession, user_input: str, provider: ProviderConfigPayload | None) -> Dict[str, Any]:
|
|
342
|
+
iteration = app.state.stream_agent_iteration_factory(_resolve_config(provider))
|
|
343
|
+
events: List[Dict[str, Any]] = []
|
|
344
|
+
async for event in process_swarm_stream(
|
|
345
|
+
session,
|
|
346
|
+
user_input,
|
|
347
|
+
store=app.state.runtime.store,
|
|
348
|
+
stream_agent_iteration=iteration,
|
|
349
|
+
tool_registry=app.state.tool_registry,
|
|
350
|
+
tool_state=app.state.tool_state,
|
|
351
|
+
):
|
|
352
|
+
events.append(event)
|
|
353
|
+
checkpoints = await app.state.runtime.store.list_checkpoints(session.id)
|
|
354
|
+
return {
|
|
355
|
+
"events": events,
|
|
356
|
+
"session": _session_payload(session),
|
|
357
|
+
"checkpoints": serialize_checkpoints(checkpoints),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async def _stream_turn(session: SwarmSession, user_input: str, provider: ProviderConfigPayload | None):
|
|
361
|
+
async def event_generator():
|
|
362
|
+
try:
|
|
363
|
+
iteration = app.state.stream_agent_iteration_factory(_resolve_config(provider))
|
|
364
|
+
async for event in process_swarm_stream(
|
|
365
|
+
session,
|
|
366
|
+
user_input,
|
|
367
|
+
store=app.state.runtime.store,
|
|
368
|
+
stream_agent_iteration=iteration,
|
|
369
|
+
tool_registry=app.state.tool_registry,
|
|
370
|
+
tool_state=app.state.tool_state,
|
|
371
|
+
):
|
|
372
|
+
yield _sse_event(event["event"], event["data"])
|
|
373
|
+
checkpoints = await app.state.runtime.store.list_checkpoints(session.id)
|
|
374
|
+
yield _sse_event("session", _session_payload(session))
|
|
375
|
+
yield _sse_event("checkpoints", {"items": serialize_checkpoints(checkpoints)})
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
yield _sse_event("error", {"message": str(exc)})
|
|
378
|
+
|
|
379
|
+
return StreamingResponse(
|
|
380
|
+
event_generator(),
|
|
381
|
+
media_type="text/event-stream",
|
|
382
|
+
headers={
|
|
383
|
+
"Cache-Control": "no-cache",
|
|
384
|
+
"Connection": "keep-alive",
|
|
385
|
+
"X-Accel-Buffering": "no",
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
@app.get("/")
|
|
390
|
+
async def root() -> Dict[str, str]:
|
|
391
|
+
return {
|
|
392
|
+
"name": "SwarmForge API",
|
|
393
|
+
"status": "ok",
|
|
394
|
+
"docs": "/docs",
|
|
395
|
+
"health": "/health",
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@app.get("/favicon.ico")
|
|
399
|
+
async def favicon() -> Response:
|
|
400
|
+
return Response(status_code=204)
|
|
401
|
+
|
|
402
|
+
@app.get("/health")
|
|
403
|
+
async def health() -> Dict[str, str]:
|
|
404
|
+
return {"status": "ok"}
|
|
405
|
+
|
|
406
|
+
@app.post("/v1/sessions")
|
|
407
|
+
async def create_session(
|
|
408
|
+
request: CreateSessionRequest = Body(..., openapi_examples={"default": CREATE_SESSION_OPENAPI_EXAMPLE})
|
|
409
|
+
) -> Dict[str, Any]:
|
|
410
|
+
swarm = request.swarm.to_runtime()
|
|
411
|
+
try:
|
|
412
|
+
session = await app.state.runtime.create_session(
|
|
413
|
+
swarm=swarm,
|
|
414
|
+
session_id=request.session_id,
|
|
415
|
+
global_variables=request.global_variables,
|
|
416
|
+
)
|
|
417
|
+
except ValueError as exc:
|
|
418
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
419
|
+
return {"session": _session_payload(session)}
|
|
420
|
+
|
|
421
|
+
@app.get("/v1/sessions/{session_id}")
|
|
422
|
+
async def get_session(session_id: str) -> Dict[str, Any]:
|
|
423
|
+
try:
|
|
424
|
+
session = await app.state.runtime.get_session(session_id)
|
|
425
|
+
except KeyError as exc:
|
|
426
|
+
raise HTTPException(status_code=404, detail="Session not found") from exc
|
|
427
|
+
checkpoints = await app.state.runtime.store.list_checkpoints(session.id)
|
|
428
|
+
return {
|
|
429
|
+
"session": _session_payload(session),
|
|
430
|
+
"checkpoints": serialize_checkpoints(checkpoints),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@app.post("/v1/sessions/{session_id}/messages")
|
|
434
|
+
async def send_message(
|
|
435
|
+
session_id: str,
|
|
436
|
+
request: SendMessageRequest = Body(..., openapi_examples=SEND_MESSAGE_OPENAPI_EXAMPLES),
|
|
437
|
+
) -> Dict[str, Any]:
|
|
438
|
+
try:
|
|
439
|
+
session = await app.state.runtime.get_session(session_id)
|
|
440
|
+
except KeyError as exc:
|
|
441
|
+
raise HTTPException(status_code=404, detail="Session not found") from exc
|
|
442
|
+
return await _run_turn(session, request.user_input, request.provider)
|
|
443
|
+
|
|
444
|
+
@app.post("/v1/sessions/{session_id}/messages/stream")
|
|
445
|
+
async def stream_message(
|
|
446
|
+
session_id: str,
|
|
447
|
+
request: SendMessageRequest = Body(..., openapi_examples=SEND_MESSAGE_OPENAPI_EXAMPLES),
|
|
448
|
+
):
|
|
449
|
+
try:
|
|
450
|
+
session = await app.state.runtime.get_session(session_id)
|
|
451
|
+
except KeyError as exc:
|
|
452
|
+
raise HTTPException(status_code=404, detail="Session not found") from exc
|
|
453
|
+
return await _stream_turn(session, request.user_input, request.provider)
|
|
454
|
+
|
|
455
|
+
@app.post("/v1/swarm/run")
|
|
456
|
+
async def run_swarm(
|
|
457
|
+
request: RunSwarmRequest = Body(..., openapi_examples=RUN_SWARM_OPENAPI_EXAMPLES)
|
|
458
|
+
) -> Dict[str, Any]:
|
|
459
|
+
swarm = request.swarm.to_runtime()
|
|
460
|
+
try:
|
|
461
|
+
session = await app.state.runtime.create_session(
|
|
462
|
+
swarm=swarm,
|
|
463
|
+
session_id=request.session_id,
|
|
464
|
+
global_variables=request.global_variables,
|
|
465
|
+
)
|
|
466
|
+
except ValueError as exc:
|
|
467
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
468
|
+
return await _run_turn(session, request.user_input, request.provider)
|
|
469
|
+
|
|
470
|
+
@app.post("/v1/swarm/run/stream")
|
|
471
|
+
async def stream_swarm_run(
|
|
472
|
+
request: RunSwarmRequest = Body(..., openapi_examples=RUN_SWARM_OPENAPI_EXAMPLES)
|
|
473
|
+
):
|
|
474
|
+
swarm = request.swarm.to_runtime()
|
|
475
|
+
try:
|
|
476
|
+
session = await app.state.runtime.create_session(
|
|
477
|
+
swarm=swarm,
|
|
478
|
+
session_id=request.session_id,
|
|
479
|
+
global_variables=request.global_variables,
|
|
480
|
+
)
|
|
481
|
+
except ValueError as exc:
|
|
482
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
483
|
+
return await _stream_turn(session, request.user_input, request.provider)
|
|
484
|
+
|
|
485
|
+
return app
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Authoring helpers for prompt generation and graph validation."""
|
|
2
|
+
|
|
3
|
+
from .prompts import META_PROMPT_EDGE, META_PROMPT_NODE, META_PROMPT_SWARM
|
|
4
|
+
from .validators import (
|
|
5
|
+
VALID_REDUCER_RULES,
|
|
6
|
+
apply_swarm_graph_payload,
|
|
7
|
+
build_generated_variables_from_swarm_payload,
|
|
8
|
+
build_swarm_definition,
|
|
9
|
+
validate_generated_edge_payload,
|
|
10
|
+
validate_generated_swarm_payload,
|
|
11
|
+
validate_swarm_definition,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"META_PROMPT_EDGE",
|
|
16
|
+
"META_PROMPT_NODE",
|
|
17
|
+
"META_PROMPT_SWARM",
|
|
18
|
+
"VALID_REDUCER_RULES",
|
|
19
|
+
"apply_swarm_graph_payload",
|
|
20
|
+
"build_generated_variables_from_swarm_payload",
|
|
21
|
+
"build_swarm_definition",
|
|
22
|
+
"validate_generated_edge_payload",
|
|
23
|
+
"validate_generated_swarm_payload",
|
|
24
|
+
"validate_swarm_definition",
|
|
25
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Prompt templates for swarm authoring workflows."""
|
|
2
|
+
|
|
3
|
+
META_PROMPT_NODE = """
|
|
4
|
+
You are the SwarmForge Node Authoring Skill.
|
|
5
|
+
|
|
6
|
+
Your job is to write the production-ready `system_prompt` text for one `swarmforge.swarm.SwarmNode`.
|
|
7
|
+
|
|
8
|
+
Framework context:
|
|
9
|
+
- This prompt becomes the value of the node's `system_prompt` field.
|
|
10
|
+
- The runtime injects routing rules, handoff guidance, visible global variables, tool schemas, and behavior config separately.
|
|
11
|
+
- Do not hardcode graph structure, downstream agents, transfer logic, or orchestration internals into the prompt.
|
|
12
|
+
- If tools exist, the prompt may describe when to use them, but it must not invent tools that were not provided.
|
|
13
|
+
|
|
14
|
+
Node inputs:
|
|
15
|
+
- Agent Name: {node_name}
|
|
16
|
+
- User Need / Intent: {user_intent}
|
|
17
|
+
- Capabilities: {capabilities_summary}
|
|
18
|
+
- Persona Guidance: {persona_summary}
|
|
19
|
+
|
|
20
|
+
Write a system prompt that:
|
|
21
|
+
- defines the node's job, scope boundaries, and success criteria
|
|
22
|
+
- explains how the listed capabilities shape the node's work
|
|
23
|
+
- tells the node what facts it should gather before acting when information is incomplete
|
|
24
|
+
- keeps the node task-focused, concrete, and usable in a real runtime
|
|
25
|
+
- uses persona guidance for tone and collaboration style only
|
|
26
|
+
- avoids delegation policy, handoff conditions, routing rules, or references to `transfer_to_agent`
|
|
27
|
+
- does not mention JSON, schemas, graph nodes, or framework implementation details
|
|
28
|
+
|
|
29
|
+
Output rules:
|
|
30
|
+
- Return only the prompt text
|
|
31
|
+
- Do not wrap it in JSON, markdown, or code fences
|
|
32
|
+
""".strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
META_PROMPT_EDGE = """
|
|
36
|
+
You are the SwarmForge Edge Authoring Skill.
|
|
37
|
+
|
|
38
|
+
Your job is to generate one SwarmForge edge payload describing when a source node should hand off to a target node.
|
|
39
|
+
|
|
40
|
+
Framework context:
|
|
41
|
+
- The result will be used as a `SwarmEdge` authoring payload.
|
|
42
|
+
- `handoff_description` becomes runtime guidance for when a transfer is valid.
|
|
43
|
+
- `required_variables` must contain only the minimum shared facts the source node should gather before transfer.
|
|
44
|
+
|
|
45
|
+
Route inputs:
|
|
46
|
+
- Source Agent: {source_name}
|
|
47
|
+
- Source Intent: {source_intent}
|
|
48
|
+
- Target Agent: {target_name}
|
|
49
|
+
- Target Intent: {target_intent}
|
|
50
|
+
|
|
51
|
+
Generate:
|
|
52
|
+
1. `handoff_description`
|
|
53
|
+
One sentence that tells the source node when the user's request should be routed to the target node.
|
|
54
|
+
2. `required_variables`
|
|
55
|
+
A list of 0 to 3 variable keys the source node should collect before the transfer.
|
|
56
|
+
|
|
57
|
+
Rules for `handoff_description`:
|
|
58
|
+
- It must require the source node to identify or confirm the user's intent before transfer.
|
|
59
|
+
- It must transfer only when the request clearly matches the target node's scope.
|
|
60
|
+
- It must not encourage handoff based on a weak keyword match.
|
|
61
|
+
- It must describe the business condition for transfer, not implementation details.
|
|
62
|
+
|
|
63
|
+
Rules for `required_variables`:
|
|
64
|
+
- Use short snake_case keys.
|
|
65
|
+
- Include only facts that the target node genuinely needs.
|
|
66
|
+
- Return an empty array if no shared variable is required.
|
|
67
|
+
- Do not invent internal-only state or transport fields.
|
|
68
|
+
|
|
69
|
+
Return only valid JSON matching this schema:
|
|
70
|
+
{{"handoff_description": "...", "required_variables": ["..."]}}
|
|
71
|
+
""".strip()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
META_PROMPT_SWARM = """
|
|
75
|
+
You are the SwarmForge Swarm Authoring Skill.
|
|
76
|
+
|
|
77
|
+
Your job is to convert a user request into a runnable SwarmForge graph payload that can be passed directly to `build_swarm_definition(...)`.
|
|
78
|
+
|
|
79
|
+
User request:
|
|
80
|
+
{global_intent}
|
|
81
|
+
|
|
82
|
+
Framework schema:
|
|
83
|
+
- Return one JSON object with `nodes`, `edges`, and optionally `variables`.
|
|
84
|
+
- The payload must match the SwarmForge authoring model used by `swarmforge.authoring.build_swarm_definition`.
|
|
85
|
+
- Favor the smallest graph that can solve the request.
|
|
86
|
+
- Use a single node when one agent can handle the task. Introduce a triage/router node only when the request clearly requires multiple specialized agents or intent-based routing.
|
|
87
|
+
|
|
88
|
+
Each node may contain:
|
|
89
|
+
- `node_key`: short stable snake_case key
|
|
90
|
+
- `name`: human-readable unique display name
|
|
91
|
+
- `intent`: clear business responsibility for the node
|
|
92
|
+
- `capabilities`: array of 2 to 5 concrete capabilities
|
|
93
|
+
- `persona`: optional tone/style guidance, or an empty string
|
|
94
|
+
- `system_prompt`: production-ready prompt text for this node
|
|
95
|
+
- `model_choice`: optional model override, or an empty string
|
|
96
|
+
- `enabled_tools`: array of tool definitions, usually `[]` unless the request clearly requires known tools
|
|
97
|
+
- `behavior_config`: object, usually `{}` unless the request clearly needs non-default behavior
|
|
98
|
+
- `is_entry_node`: boolean
|
|
99
|
+
|
|
100
|
+
Each edge may contain:
|
|
101
|
+
- `source_node_key`
|
|
102
|
+
- `target_node_key`
|
|
103
|
+
- `handoff_description`
|
|
104
|
+
- `required_variables`
|
|
105
|
+
|
|
106
|
+
Each top-level variable may contain:
|
|
107
|
+
- `key_name`
|
|
108
|
+
- `description`
|
|
109
|
+
- `reducer_rule` where the value is `overwrite`, `append`, or `keep_first`
|
|
110
|
+
|
|
111
|
+
Generation rules:
|
|
112
|
+
- Produce between 1 and 6 nodes.
|
|
113
|
+
- Mark exactly one node as `is_entry_node=true`.
|
|
114
|
+
- Every node must have a concrete and non-overlapping responsibility.
|
|
115
|
+
- Every `intent` must be specific enough to drive routing and evaluation.
|
|
116
|
+
- Every `system_prompt` must be ready to run and must stay inside the node's own scope.
|
|
117
|
+
- Do not put graph-wide routing rules inside node `system_prompt` values.
|
|
118
|
+
- Every edge must reference existing node keys and may not self-target.
|
|
119
|
+
- Only create edges for realistic handoffs.
|
|
120
|
+
- `required_variables` should be the minimum facts that the target node needs from the source node.
|
|
121
|
+
- Only create top-level `variables` for facts that are shared across nodes or required by handoffs.
|
|
122
|
+
- Use `overwrite` unless there is a clear reason to preserve the first value or accumulate a list.
|
|
123
|
+
- If tools are not explicitly available from the request context, use `enabled_tools: []`.
|
|
124
|
+
- If no custom behavior is necessary, use `behavior_config: {}`.
|
|
125
|
+
- If no model override is necessary, use `model_choice: ""`.
|
|
126
|
+
- Do not invent framework fields outside the schema above.
|
|
127
|
+
|
|
128
|
+
Return only valid JSON with this shape:
|
|
129
|
+
{
|
|
130
|
+
"nodes": [
|
|
131
|
+
{
|
|
132
|
+
"node_key": "...",
|
|
133
|
+
"name": "...",
|
|
134
|
+
"intent": "...",
|
|
135
|
+
"capabilities": ["..."],
|
|
136
|
+
"persona": "...",
|
|
137
|
+
"system_prompt": "...",
|
|
138
|
+
"model_choice": "",
|
|
139
|
+
"enabled_tools": [],
|
|
140
|
+
"behavior_config": {},
|
|
141
|
+
"is_entry_node": true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"edges": [
|
|
145
|
+
{
|
|
146
|
+
"source_node_key": "...",
|
|
147
|
+
"target_node_key": "...",
|
|
148
|
+
"handoff_description": "...",
|
|
149
|
+
"required_variables": ["..."]
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"variables": [
|
|
153
|
+
{
|
|
154
|
+
"key_name": "...",
|
|
155
|
+
"description": "...",
|
|
156
|
+
"reducer_rule": "overwrite"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Output rules:
|
|
162
|
+
- Return raw JSON only
|
|
163
|
+
- Do not use markdown fences
|
|
164
|
+
- Do not include explanations before or after the JSON
|
|
165
|
+
""".strip()
|