polos-sdk 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.
- polos/__init__.py +105 -0
- polos/agents/__init__.py +7 -0
- polos/agents/agent.py +746 -0
- polos/agents/conversation_history.py +121 -0
- polos/agents/stop_conditions.py +280 -0
- polos/agents/stream.py +635 -0
- polos/core/__init__.py +0 -0
- polos/core/context.py +143 -0
- polos/core/state.py +26 -0
- polos/core/step.py +1380 -0
- polos/core/workflow.py +1192 -0
- polos/features/__init__.py +0 -0
- polos/features/events.py +456 -0
- polos/features/schedules.py +110 -0
- polos/features/tracing.py +605 -0
- polos/features/wait.py +82 -0
- polos/llm/__init__.py +9 -0
- polos/llm/generate.py +152 -0
- polos/llm/providers/__init__.py +5 -0
- polos/llm/providers/anthropic.py +615 -0
- polos/llm/providers/azure.py +42 -0
- polos/llm/providers/base.py +196 -0
- polos/llm/providers/fireworks.py +41 -0
- polos/llm/providers/gemini.py +40 -0
- polos/llm/providers/groq.py +40 -0
- polos/llm/providers/openai.py +1021 -0
- polos/llm/providers/together.py +40 -0
- polos/llm/stream.py +183 -0
- polos/middleware/__init__.py +0 -0
- polos/middleware/guardrail.py +148 -0
- polos/middleware/guardrail_executor.py +253 -0
- polos/middleware/hook.py +164 -0
- polos/middleware/hook_executor.py +104 -0
- polos/runtime/__init__.py +0 -0
- polos/runtime/batch.py +87 -0
- polos/runtime/client.py +841 -0
- polos/runtime/queue.py +42 -0
- polos/runtime/worker.py +1365 -0
- polos/runtime/worker_server.py +249 -0
- polos/tools/__init__.py +0 -0
- polos/tools/tool.py +587 -0
- polos/types/__init__.py +23 -0
- polos/types/types.py +116 -0
- polos/utils/__init__.py +27 -0
- polos/utils/agent.py +27 -0
- polos/utils/client_context.py +41 -0
- polos/utils/config.py +12 -0
- polos/utils/output_schema.py +311 -0
- polos/utils/retry.py +47 -0
- polos/utils/serializer.py +167 -0
- polos/utils/tracing.py +27 -0
- polos/utils/worker_singleton.py +40 -0
- polos_sdk-0.1.0.dist-info/METADATA +650 -0
- polos_sdk-0.1.0.dist-info/RECORD +55 -0
- polos_sdk-0.1.0.dist-info/WHEEL +4 -0
polos/agents/agent.py
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"""Agent decorator and Agent class for LLM-powered workflows."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..core.context import AgentContext
|
|
11
|
+
from ..core.workflow import _WORKFLOW_REGISTRY, Workflow, _execution_context
|
|
12
|
+
from ..runtime.client import ExecutionHandle, PolosClient
|
|
13
|
+
from ..runtime.queue import Queue
|
|
14
|
+
from ..types.types import AgentConfig, AgentResult
|
|
15
|
+
from ..utils.output_schema import convert_output_schema
|
|
16
|
+
from ..utils.serializer import deserialize_agent_result
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentRunConfig:
|
|
22
|
+
"""Configuration for batch agent execution."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
agent: "Agent",
|
|
27
|
+
input: str | list[dict[str, Any]],
|
|
28
|
+
session_id: str | None = None,
|
|
29
|
+
conversation_id: str | None = None,
|
|
30
|
+
user_id: str | None = None,
|
|
31
|
+
streaming: bool = False,
|
|
32
|
+
initial_state: BaseModel | dict[str, Any] | None = None,
|
|
33
|
+
run_timeout_seconds: int | None = None,
|
|
34
|
+
**kwargs,
|
|
35
|
+
):
|
|
36
|
+
self.agent = agent
|
|
37
|
+
self.input = input
|
|
38
|
+
self.session_id = session_id
|
|
39
|
+
self.conversation_id = conversation_id
|
|
40
|
+
self.user_id = user_id
|
|
41
|
+
self.streaming = streaming
|
|
42
|
+
self.initial_state = initial_state
|
|
43
|
+
self.run_timeout_seconds = run_timeout_seconds
|
|
44
|
+
self.kwargs = kwargs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StreamResult:
|
|
48
|
+
"""Result object for streaming agent responses.
|
|
49
|
+
|
|
50
|
+
This class wraps ExecutionHandle and adds event polling capabilities.
|
|
51
|
+
agent_run_id is the same as execution_id.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, execution_handle: ExecutionHandle, client: PolosClient):
|
|
55
|
+
"""Initialize StreamResult from an ExecutionHandle.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
execution_handle: The ExecutionHandle from agent.invoke() when streaming is True
|
|
59
|
+
"""
|
|
60
|
+
# Store reference to the ExecutionHandle
|
|
61
|
+
self.client = client
|
|
62
|
+
self.handle = execution_handle
|
|
63
|
+
|
|
64
|
+
# Ensure root_execution_id is set (use execution_id if root_execution_id is None)
|
|
65
|
+
if not execution_handle.root_execution_id:
|
|
66
|
+
execution_handle.root_execution_id = execution_handle.id
|
|
67
|
+
|
|
68
|
+
# Expose commonly used properties for convenience
|
|
69
|
+
self.agent_run_id = execution_handle.id # execution_id is the same as agent_run_id
|
|
70
|
+
self.topic = (
|
|
71
|
+
f"workflow:{execution_handle.root_execution_id}" # Topic derived from root_execution_id
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Delegate ExecutionHandle properties/methods
|
|
75
|
+
@property
|
|
76
|
+
def id(self) -> str:
|
|
77
|
+
"""Get execution ID (same as agent_run_id)."""
|
|
78
|
+
return self.handle.id
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def workflow_id(self) -> str | None:
|
|
82
|
+
"""Get workflow ID."""
|
|
83
|
+
return self.handle.workflow_id
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def created_at(self) -> str | None:
|
|
87
|
+
"""Get creation timestamp."""
|
|
88
|
+
return self.handle.created_at
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def parent_execution_id(self) -> str | None:
|
|
92
|
+
"""Get parent execution ID."""
|
|
93
|
+
return self.handle.parent_execution_id
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def root_execution_id(self) -> str | None:
|
|
97
|
+
"""Get root execution ID."""
|
|
98
|
+
return self.handle.root_execution_id
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def session_id(self) -> str | None:
|
|
102
|
+
"""Get session ID."""
|
|
103
|
+
return self.handle.session_id
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def user_id(self) -> str | None:
|
|
107
|
+
"""Get user ID."""
|
|
108
|
+
return self.handle.user_id
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def step_key(self) -> str | None:
|
|
112
|
+
"""Get step key."""
|
|
113
|
+
return self.handle.step_key
|
|
114
|
+
|
|
115
|
+
async def get(self) -> dict[str, Any]:
|
|
116
|
+
"""Get the current status of the execution."""
|
|
117
|
+
return await self.handle.get(self.client)
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict[str, Any]:
|
|
120
|
+
"""Convert to dictionary."""
|
|
121
|
+
return self.handle.to_dict()
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
"""String representation."""
|
|
125
|
+
return f"StreamResult(agent_run_id={self.agent_run_id}, topic={self.topic})"
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def text_chunks(self) -> "TextChunkIterator":
|
|
129
|
+
"""Iterate text chunks only.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
async for chunk in result.text_chunks:
|
|
133
|
+
print(chunk, end="")
|
|
134
|
+
"""
|
|
135
|
+
return TextChunkIterator(self)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def events(self) -> "FullEventIterator":
|
|
139
|
+
"""Iterate all events (text, tool calls, etc.).
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
async for event in result.events:
|
|
143
|
+
if event.event_type == "text_delta":
|
|
144
|
+
print(event.data.get("content", ""))
|
|
145
|
+
"""
|
|
146
|
+
return FullEventIterator(self)
|
|
147
|
+
|
|
148
|
+
async def text(self) -> str:
|
|
149
|
+
"""Get final accumulated text.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
final_text = await result.text()
|
|
153
|
+
print(final_text)
|
|
154
|
+
"""
|
|
155
|
+
accumulated = ""
|
|
156
|
+
async for chunk in self.text_chunks:
|
|
157
|
+
if chunk:
|
|
158
|
+
accumulated += chunk
|
|
159
|
+
return accumulated
|
|
160
|
+
|
|
161
|
+
async def result(self) -> AgentResult:
|
|
162
|
+
"""Get complete result with usage, tool calls, etc.
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
final_result = await result.result()
|
|
166
|
+
print(final_result.result)
|
|
167
|
+
print(final_result.usage)
|
|
168
|
+
"""
|
|
169
|
+
result_data = None
|
|
170
|
+
from ..features.events import stream_workflow
|
|
171
|
+
|
|
172
|
+
async for event in stream_workflow(
|
|
173
|
+
client=self.client, workflow_run_id=self.agent_run_id, last_sequence_id=0
|
|
174
|
+
):
|
|
175
|
+
if (
|
|
176
|
+
event.event_type == "agent_finish"
|
|
177
|
+
and event.data.get("_metadata", {}).get("execution_id") == self.agent_run_id
|
|
178
|
+
):
|
|
179
|
+
result_data = event.data.get("result")
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
if result_data:
|
|
183
|
+
agent_result = AgentResult(
|
|
184
|
+
agent_run_id=result_data.get("agent_run_id"),
|
|
185
|
+
result=result_data.get("result"),
|
|
186
|
+
result_schema=result_data.get("result_schema"),
|
|
187
|
+
tool_results=result_data.get("tool_results"),
|
|
188
|
+
total_steps=result_data.get("total_steps", 0),
|
|
189
|
+
usage=result_data.get("usage"),
|
|
190
|
+
)
|
|
191
|
+
return await deserialize_agent_result(agent_result)
|
|
192
|
+
|
|
193
|
+
raise Exception("No result found for agent run id: " + self.agent_run_id)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AgentStreamHandle:
|
|
197
|
+
"""Handle for streaming agent responses."""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
client: PolosClient,
|
|
202
|
+
agent_run_id: str,
|
|
203
|
+
root_execution_id: str,
|
|
204
|
+
created_at: str | None = None,
|
|
205
|
+
):
|
|
206
|
+
self.client = client
|
|
207
|
+
self.agent_run_id = agent_run_id
|
|
208
|
+
self.topic = f"workflow:{root_execution_id or agent_run_id}"
|
|
209
|
+
self.last_valid_event_id = None # Track last valid event (skip invalid ones)
|
|
210
|
+
self.created_at = created_at # Timestamp when agent_run was created
|
|
211
|
+
|
|
212
|
+
async def __aiter__(self):
|
|
213
|
+
"""Async iterator that yields chunks from events via SSE.
|
|
214
|
+
|
|
215
|
+
Uses events.stream_workflow() to stream events from the orchestrator.
|
|
216
|
+
Filters out invalid events (from failed/retried attempts).
|
|
217
|
+
"""
|
|
218
|
+
from ..features.events import stream_workflow
|
|
219
|
+
|
|
220
|
+
# Convert created_at string to datetime if provided
|
|
221
|
+
last_timestamp = None
|
|
222
|
+
if self.created_at:
|
|
223
|
+
import contextlib
|
|
224
|
+
|
|
225
|
+
with contextlib.suppress(ValueError, AttributeError):
|
|
226
|
+
last_timestamp = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
|
|
227
|
+
|
|
228
|
+
# Use events.stream_workflow() with agent_run_id
|
|
229
|
+
async for event in stream_workflow(
|
|
230
|
+
client=self.client, workflow_run_id=self.agent_run_id, last_timestamp=last_timestamp
|
|
231
|
+
):
|
|
232
|
+
event_type = event.event_type
|
|
233
|
+
self.last_valid_event_id = event.id
|
|
234
|
+
yield event
|
|
235
|
+
|
|
236
|
+
# Handle workflow finish event
|
|
237
|
+
if (
|
|
238
|
+
event_type == "agent_finish"
|
|
239
|
+
and event.data.get("_metadata", {}).get("execution_id") == self.agent_run_id
|
|
240
|
+
):
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
def __repr__(self) -> str:
|
|
244
|
+
return (
|
|
245
|
+
f"AgentStreamHandle(agent_run_id={self.agent_run_id}, "
|
|
246
|
+
f"root_execution_id={self.root_execution_id or self.agent_run_id})"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TextChunkIterator:
|
|
251
|
+
"""Iterator for text chunks only."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, result: "StreamResult"):
|
|
254
|
+
self.result = result
|
|
255
|
+
self._handle_iter = None
|
|
256
|
+
|
|
257
|
+
def __aiter__(self):
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
async def __anext__(self):
|
|
261
|
+
if self._handle_iter is None:
|
|
262
|
+
handle = AgentStreamHandle(
|
|
263
|
+
self.result.client,
|
|
264
|
+
self.result.id,
|
|
265
|
+
self.result.root_execution_id,
|
|
266
|
+
self.result.created_at,
|
|
267
|
+
)
|
|
268
|
+
self._handle_iter = handle.__aiter__()
|
|
269
|
+
|
|
270
|
+
# Filter for text chunks only
|
|
271
|
+
while True:
|
|
272
|
+
try:
|
|
273
|
+
event = await self._handle_iter.__anext__()
|
|
274
|
+
if event.event_type == "text_delta":
|
|
275
|
+
return event.data.get("content", "")
|
|
276
|
+
except StopAsyncIteration:
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class FullEventIterator:
|
|
281
|
+
"""Iterator for all events."""
|
|
282
|
+
|
|
283
|
+
def __init__(self, result: "StreamResult"):
|
|
284
|
+
self.result = result
|
|
285
|
+
self._handle_iter = None
|
|
286
|
+
|
|
287
|
+
def __aiter__(self):
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
async def __anext__(self):
|
|
291
|
+
if self._handle_iter is None:
|
|
292
|
+
handle = AgentStreamHandle(
|
|
293
|
+
self.result.client,
|
|
294
|
+
self.result.id,
|
|
295
|
+
self.result.root_execution_id,
|
|
296
|
+
self.result.created_at,
|
|
297
|
+
)
|
|
298
|
+
self._handle_iter = handle.__aiter__()
|
|
299
|
+
|
|
300
|
+
return await self._handle_iter.__anext__()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Agent(Workflow):
|
|
304
|
+
"""
|
|
305
|
+
Agent class that inherits from Workflow - agents are durable, retriable
|
|
306
|
+
workflows with LLM capabilities.
|
|
307
|
+
|
|
308
|
+
Agents can be triggered, composed with other tasks, and benefit from all task features:
|
|
309
|
+
- Automatic checkpointing and retry
|
|
310
|
+
- Queue management and concurrency control
|
|
311
|
+
- Parent-child execution tracking
|
|
312
|
+
- Observability and tracing
|
|
313
|
+
|
|
314
|
+
Usage:
|
|
315
|
+
# Define agent
|
|
316
|
+
weather_agent = Agent(
|
|
317
|
+
id="weather-agent",
|
|
318
|
+
provider="openai",
|
|
319
|
+
model="gpt-4o",
|
|
320
|
+
system_prompt="You are a helpful weather assistant",
|
|
321
|
+
tools=[get_weather]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Start the agent
|
|
325
|
+
result = await weather_agent.invoke({
|
|
326
|
+
"input": "What's the weather in NYC?",
|
|
327
|
+
"streaming": False
|
|
328
|
+
})
|
|
329
|
+
print(result["text"])
|
|
330
|
+
|
|
331
|
+
# Or Start the agent with streaming
|
|
332
|
+
stream_handle = await weather_agent.stream({
|
|
333
|
+
"input": "What's the weather in NYC?",
|
|
334
|
+
"streaming": True
|
|
335
|
+
})
|
|
336
|
+
async for chunk in stream_handle.events:
|
|
337
|
+
print(chunk.data.get("content", ""), end="", flush=True)
|
|
338
|
+
|
|
339
|
+
# Or use convenience method to start the agent and wait for it to complete
|
|
340
|
+
result = await weather_agent.run("What's the weather in NYC?")
|
|
341
|
+
|
|
342
|
+
# Stream response (from within a task)
|
|
343
|
+
stream_result = await weather_agent.stream("What's the weather?")
|
|
344
|
+
async for event in stream_result.events:
|
|
345
|
+
if event.event_type == "text_delta":
|
|
346
|
+
print(event.data.get("content", ""), end="", flush=True)
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
id: str, # Required: task ID
|
|
352
|
+
provider: str,
|
|
353
|
+
model: str,
|
|
354
|
+
system_prompt: str | None = None,
|
|
355
|
+
tools: list[Any] | None = None,
|
|
356
|
+
temperature: float | None = None,
|
|
357
|
+
max_output_tokens: int | None = None,
|
|
358
|
+
provider_base_url: str | None = None,
|
|
359
|
+
provider_llm_api: str | None = None,
|
|
360
|
+
queue: str | Queue | dict[str, Any] | None = None,
|
|
361
|
+
stop_conditions: list[Any] | None = None,
|
|
362
|
+
output_schema: type[Any] | None = None, # Pydantic model class for structured output
|
|
363
|
+
on_start: Callable | list[Callable] | None = None,
|
|
364
|
+
on_end: Callable | list[Callable] | None = None,
|
|
365
|
+
on_agent_step_start: Callable | list[Callable] | None = None,
|
|
366
|
+
on_agent_step_end: Callable | list[Callable] | None = None,
|
|
367
|
+
on_tool_start: Callable | list[Callable] | None = None,
|
|
368
|
+
on_tool_end: Callable | list[Callable] | None = None,
|
|
369
|
+
guardrails: Callable | str | list[Callable | str] | None = None,
|
|
370
|
+
guardrail_max_retries: int = 2,
|
|
371
|
+
conversation_history: int = 10, # Number of messages to keep
|
|
372
|
+
):
|
|
373
|
+
# Parse queue configuration (same as task decorator)
|
|
374
|
+
queue_name: str | None = None
|
|
375
|
+
queue_concurrency_limit: int | None = None
|
|
376
|
+
|
|
377
|
+
if queue is not None:
|
|
378
|
+
if isinstance(queue, str):
|
|
379
|
+
queue_name = queue
|
|
380
|
+
elif isinstance(queue, Queue):
|
|
381
|
+
queue_name = queue.name
|
|
382
|
+
queue_concurrency_limit = queue.concurrency_limit
|
|
383
|
+
elif isinstance(queue, dict):
|
|
384
|
+
queue_name = queue.get("name", id)
|
|
385
|
+
queue_concurrency_limit = queue.get("concurrency_limit")
|
|
386
|
+
|
|
387
|
+
# Initialize as Workflow with agent execution function
|
|
388
|
+
super().__init__(
|
|
389
|
+
id=id,
|
|
390
|
+
func=self._agent_execute,
|
|
391
|
+
workflow_type="agent",
|
|
392
|
+
queue_name=queue_name,
|
|
393
|
+
queue_concurrency_limit=queue_concurrency_limit,
|
|
394
|
+
on_start=on_start,
|
|
395
|
+
on_end=on_end,
|
|
396
|
+
output_schema=AgentResult,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Agent configuration
|
|
400
|
+
self.provider = provider
|
|
401
|
+
|
|
402
|
+
# Store provider_base_url and provider_llm_api for AgentConfig
|
|
403
|
+
self.provider_base_url = provider_base_url
|
|
404
|
+
self.provider_llm_api = provider_llm_api
|
|
405
|
+
|
|
406
|
+
self.model = model
|
|
407
|
+
self.system_prompt = system_prompt
|
|
408
|
+
self.tools = tools or []
|
|
409
|
+
self.temperature = temperature
|
|
410
|
+
self.max_output_tokens = max_output_tokens
|
|
411
|
+
self.result_output_schema = output_schema # Pydantic model class
|
|
412
|
+
self.stop_conditions: list[Callable] = []
|
|
413
|
+
|
|
414
|
+
# Agent-specific lifecycle hooks
|
|
415
|
+
self.on_agent_step_start = self._normalize_hooks(on_agent_step_start)
|
|
416
|
+
self.on_agent_step_end = self._normalize_hooks(on_agent_step_end)
|
|
417
|
+
self.on_tool_start = self._normalize_hooks(on_tool_start)
|
|
418
|
+
self.on_tool_end = self._normalize_hooks(on_tool_end)
|
|
419
|
+
|
|
420
|
+
# Guardrails
|
|
421
|
+
self.guardrails = self._normalize_guardrails(guardrails)
|
|
422
|
+
self.guardrail_max_retries = guardrail_max_retries
|
|
423
|
+
|
|
424
|
+
# Conversation history
|
|
425
|
+
self.conversation_history = conversation_history
|
|
426
|
+
|
|
427
|
+
# Convert Pydantic model to JSON schema if provided
|
|
428
|
+
self._output_json_schema, self._output_schema_name = convert_output_schema(
|
|
429
|
+
output_schema, context_id=self.id
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Normalize and validate stop conditions:
|
|
433
|
+
# Only accept callables (configured callables from @stop_condition decorator)
|
|
434
|
+
for sc in stop_conditions or []:
|
|
435
|
+
if not callable(sc):
|
|
436
|
+
raise TypeError(
|
|
437
|
+
f"Invalid stop_condition {sc!r} for agent '{id}'. "
|
|
438
|
+
f"Each stop condition must be a callable (use @stop_condition decorator)."
|
|
439
|
+
)
|
|
440
|
+
self.stop_conditions.append(sc)
|
|
441
|
+
|
|
442
|
+
# Register locally (for in-process calls)
|
|
443
|
+
_WORKFLOW_REGISTRY[id] = self
|
|
444
|
+
|
|
445
|
+
def _normalize_guardrails(
|
|
446
|
+
self, guardrails: Callable | str | list[Callable | str] | None
|
|
447
|
+
) -> list[Callable | str]:
|
|
448
|
+
"""Normalize guardrails to a list of callables or strings.
|
|
449
|
+
|
|
450
|
+
Accepts:
|
|
451
|
+
- None: Returns empty list
|
|
452
|
+
- Callable: Single guardrail callable
|
|
453
|
+
- str: Single string guardrail
|
|
454
|
+
- List[Union[Callable, str]]: List of guardrail callables or strings
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
if guardrails is None:
|
|
458
|
+
return []
|
|
459
|
+
if callable(guardrails) or isinstance(guardrails, str):
|
|
460
|
+
return [guardrails]
|
|
461
|
+
if isinstance(guardrails, list):
|
|
462
|
+
result = []
|
|
463
|
+
for gr in guardrails:
|
|
464
|
+
if callable(gr) or isinstance(gr, str):
|
|
465
|
+
result.append(gr)
|
|
466
|
+
else:
|
|
467
|
+
raise TypeError(
|
|
468
|
+
f"Invalid guardrail type: {type(gr)}. Expected a callable "
|
|
469
|
+
f"(function decorated with @guardrail) or a string."
|
|
470
|
+
)
|
|
471
|
+
return result
|
|
472
|
+
raise TypeError(
|
|
473
|
+
f"Invalid guardrails type: {type(guardrails)}. Expected callable, "
|
|
474
|
+
f"string, or List[Union[callable, str]]."
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
async def _agent_execute(self, ctx: AgentContext, payload: dict[str, Any]) -> dict[str, Any]:
|
|
478
|
+
"""
|
|
479
|
+
Internal execution function - runs the agent and collects streaming results.
|
|
480
|
+
|
|
481
|
+
This is called by the Workflow execution framework.
|
|
482
|
+
"""
|
|
483
|
+
# Extract input
|
|
484
|
+
if not isinstance(payload, dict):
|
|
485
|
+
raise TypeError(
|
|
486
|
+
f"Payload must be a dict, got {type(payload).__name__} for agent {self.id}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
input_data = payload.get("input")
|
|
490
|
+
streaming = payload.get("streaming", False) # Whether to stream or return final result
|
|
491
|
+
provider_kwargs = payload.get(
|
|
492
|
+
"provider_kwargs", {}
|
|
493
|
+
) # Additional kwargs to pass to provider
|
|
494
|
+
conversation_id = payload.get("conversation_id") # Conversation ID for conversation history
|
|
495
|
+
|
|
496
|
+
# Build agent config
|
|
497
|
+
agent_config = AgentConfig(
|
|
498
|
+
name=self.id,
|
|
499
|
+
provider=self.provider,
|
|
500
|
+
model=self.model,
|
|
501
|
+
tools=self._build_tools_schema(),
|
|
502
|
+
system_prompt=self.system_prompt,
|
|
503
|
+
max_output_tokens=self.max_output_tokens,
|
|
504
|
+
temperature=self.temperature,
|
|
505
|
+
provider_base_url=self.provider_base_url,
|
|
506
|
+
provider_llm_api=self.provider_llm_api,
|
|
507
|
+
provider_kwargs=provider_kwargs if provider_kwargs else None,
|
|
508
|
+
output_schema=self._output_json_schema,
|
|
509
|
+
output_schema_name=self._output_schema_name,
|
|
510
|
+
guardrail_max_retries=self.guardrail_max_retries,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Create agent_run record using step.run() for durable execution
|
|
514
|
+
# agent_run_id is now the same as execution_id
|
|
515
|
+
agent_run_id = ctx.execution_id
|
|
516
|
+
|
|
517
|
+
# Update context with conversation_id if provided
|
|
518
|
+
if conversation_id:
|
|
519
|
+
ctx.conversation_id = conversation_id
|
|
520
|
+
else:
|
|
521
|
+
ctx.conversation_id = await ctx.step.uuid("new_conversation_id")
|
|
522
|
+
|
|
523
|
+
# Always call _agent_stream_function with streaming parameter
|
|
524
|
+
from .stream import _agent_stream_function
|
|
525
|
+
|
|
526
|
+
result = await _agent_stream_function(
|
|
527
|
+
ctx,
|
|
528
|
+
{
|
|
529
|
+
"agent_run_id": agent_run_id,
|
|
530
|
+
"name": self.id,
|
|
531
|
+
"agent_config": agent_config,
|
|
532
|
+
"input": input_data,
|
|
533
|
+
"session_id": ctx.session_id,
|
|
534
|
+
"conversation_id": ctx.conversation_id,
|
|
535
|
+
"user_id": ctx.user_id,
|
|
536
|
+
"streaming": streaming,
|
|
537
|
+
},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
def _build_tools_schema(self) -> list[Any]:
|
|
543
|
+
"""Build tools schema from tool list."""
|
|
544
|
+
tools_schema = []
|
|
545
|
+
for tool in self.tools:
|
|
546
|
+
# Tools are Tool objects with _tool_description and _tool_parameters attributes
|
|
547
|
+
# The tool name is the task's id
|
|
548
|
+
if hasattr(tool, "id") and hasattr(tool, "_tool_parameters"):
|
|
549
|
+
tools_schema.append(
|
|
550
|
+
{
|
|
551
|
+
"type": "function",
|
|
552
|
+
"name": tool.id,
|
|
553
|
+
"description": getattr(tool, "_tool_description", ""),
|
|
554
|
+
"parameters": tool._tool_parameters,
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
# LLM built-in tool
|
|
559
|
+
tools_schema.append(tool)
|
|
560
|
+
return tools_schema
|
|
561
|
+
|
|
562
|
+
# Convenience methods for better DX
|
|
563
|
+
async def run(
|
|
564
|
+
self,
|
|
565
|
+
client: PolosClient,
|
|
566
|
+
input: str | list[dict[str, Any]],
|
|
567
|
+
initial_state: BaseModel | dict[str, Any] | None = None,
|
|
568
|
+
session_id: str | None = None,
|
|
569
|
+
conversation_id: str | None = None,
|
|
570
|
+
user_id: str | None = None,
|
|
571
|
+
timeout: float | None = 600.0,
|
|
572
|
+
**kwargs,
|
|
573
|
+
) -> AgentResult:
|
|
574
|
+
"""
|
|
575
|
+
Run agent and return final result (non-streaming).
|
|
576
|
+
|
|
577
|
+
This method cannot be called from within an execution context
|
|
578
|
+
(e.g., from within a workflow).
|
|
579
|
+
Use step.agent_run() or step.invoke_and_wait() to call agents from within workflows.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
client: PolosClient instance
|
|
583
|
+
input: String or array of input items (multimodal)
|
|
584
|
+
session_id: Optional session ID
|
|
585
|
+
conversation_id: Optional conversation ID for conversation history
|
|
586
|
+
user_id: Optional user ID
|
|
587
|
+
timeout: Optional timeout in seconds (default: 600 seconds / 10 minutes)
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
AgentResult with result, usage, tool_results, total_steps, and agent_run_id
|
|
591
|
+
|
|
592
|
+
Raises:
|
|
593
|
+
WorkflowTimeoutError: If the execution exceeds the timeout
|
|
594
|
+
|
|
595
|
+
Example:
|
|
596
|
+
result = await weather_agent.run("What's the weather in NYC?")
|
|
597
|
+
print(result.result)
|
|
598
|
+
print(result.usage)
|
|
599
|
+
print(result.tool_results)
|
|
600
|
+
"""
|
|
601
|
+
# Build agent-specific payload dict
|
|
602
|
+
agent_payload = {
|
|
603
|
+
"input": input,
|
|
604
|
+
"session_id": session_id,
|
|
605
|
+
"conversation_id": conversation_id,
|
|
606
|
+
"user_id": user_id,
|
|
607
|
+
"streaming": False,
|
|
608
|
+
"provider_kwargs": kwargs, # Pass kwargs to provider
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Call parent run() method with agent payload
|
|
612
|
+
return await super().run(
|
|
613
|
+
client=client,
|
|
614
|
+
payload=agent_payload,
|
|
615
|
+
initial_state=initial_state,
|
|
616
|
+
session_id=session_id,
|
|
617
|
+
user_id=user_id,
|
|
618
|
+
timeout=timeout,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
async def stream(
|
|
622
|
+
self,
|
|
623
|
+
client: PolosClient,
|
|
624
|
+
input: str | list[dict[str, Any]],
|
|
625
|
+
initial_state: BaseModel | dict[str, Any] | None = None,
|
|
626
|
+
session_id: str | None = None,
|
|
627
|
+
conversation_id: str | None = None,
|
|
628
|
+
user_id: str | None = None,
|
|
629
|
+
run_timeout_seconds: int | None = None,
|
|
630
|
+
**kwargs,
|
|
631
|
+
) -> "StreamResult":
|
|
632
|
+
"""
|
|
633
|
+
Start streaming agent response.
|
|
634
|
+
|
|
635
|
+
This method cannot be called from within an execution context
|
|
636
|
+
(e.g., from within a workflow).
|
|
637
|
+
Use step.agent_run() or step.invoke_and_wait() to call agents from within workflows.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
client: PolosClient instance
|
|
641
|
+
input: String or array of input items (multimodal)
|
|
642
|
+
session_id: Optional session ID
|
|
643
|
+
conversation_id: Optional conversation ID for conversation history
|
|
644
|
+
user_id: Optional user ID
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
StreamResult with event polling capabilities (wraps ExecutionHandle)
|
|
648
|
+
|
|
649
|
+
Example:
|
|
650
|
+
result = await agent.stream("What's the weather?")
|
|
651
|
+
|
|
652
|
+
# Option 1: Iterate text chunks only
|
|
653
|
+
async for chunk in result.text_chunks:
|
|
654
|
+
print(chunk, end="")
|
|
655
|
+
|
|
656
|
+
# Option 2: Get full events (text, tool calls, etc.)
|
|
657
|
+
async for event in result.events:
|
|
658
|
+
if event.event_type == "text_delta":
|
|
659
|
+
print(event.data.get("content", ""))
|
|
660
|
+
elif event.event_type == "tool_call":
|
|
661
|
+
tool_name = (
|
|
662
|
+
event.data.get("tool_call", {})
|
|
663
|
+
.get("function", {})
|
|
664
|
+
.get("name")
|
|
665
|
+
)
|
|
666
|
+
print(f"Tool: {tool_name}")
|
|
667
|
+
|
|
668
|
+
# Option 3: Get final accumulated text
|
|
669
|
+
final_text = await result.text()
|
|
670
|
+
|
|
671
|
+
# Option 4: Get complete result with usage, tool calls
|
|
672
|
+
final_result = await result.result()
|
|
673
|
+
print(final_result["result"])
|
|
674
|
+
print(final_result["usage"])
|
|
675
|
+
"""
|
|
676
|
+
# Check if we're in an execution context - fail if we are
|
|
677
|
+
|
|
678
|
+
if _execution_context.get() is not None:
|
|
679
|
+
raise RuntimeError(
|
|
680
|
+
"agent.stream() cannot be called from within a workflow or agent. "
|
|
681
|
+
"Use step.agent_run() or step.invoke_and_wait() to call agents "
|
|
682
|
+
"from within workflows."
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Invoke agent task asynchronously (returns ExecutionHandle immediately)
|
|
686
|
+
handle = await self.invoke(
|
|
687
|
+
client=client,
|
|
688
|
+
payload={
|
|
689
|
+
"input": input,
|
|
690
|
+
"session_id": session_id,
|
|
691
|
+
"user_id": user_id,
|
|
692
|
+
"streaming": True,
|
|
693
|
+
"provider_kwargs": kwargs, # Pass kwargs to provider
|
|
694
|
+
},
|
|
695
|
+
initial_state=initial_state,
|
|
696
|
+
run_timeout_seconds=run_timeout_seconds,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Wrap ExecutionHandle in StreamResult
|
|
700
|
+
return StreamResult(handle, client)
|
|
701
|
+
|
|
702
|
+
def with_input(
|
|
703
|
+
self,
|
|
704
|
+
input: str | list[dict[str, Any]],
|
|
705
|
+
initial_state: BaseModel | dict[str, Any] | None = None,
|
|
706
|
+
session_id: str | None = None,
|
|
707
|
+
conversation_id: str | None = None,
|
|
708
|
+
user_id: str | None = None,
|
|
709
|
+
streaming: bool = False,
|
|
710
|
+
run_timeout_seconds: int | None = None,
|
|
711
|
+
**kwargs,
|
|
712
|
+
) -> AgentRunConfig:
|
|
713
|
+
"""
|
|
714
|
+
Prepare agent for batch execution with all run() params.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
input: Text string or messages array
|
|
718
|
+
session_id: Optional session identifier
|
|
719
|
+
user_id: Optional user identifier
|
|
720
|
+
streaming: Whether to enable streaming (default: False)
|
|
721
|
+
**kwargs: Additional params (temperature, max_tokens, etc.)
|
|
722
|
+
|
|
723
|
+
Usage:
|
|
724
|
+
results = await batch.run([
|
|
725
|
+
grammar_agent.with_input(
|
|
726
|
+
"Check this",
|
|
727
|
+
session_id="sess_123",
|
|
728
|
+
user_id="user_456"
|
|
729
|
+
),
|
|
730
|
+
tone_agent.with_input(
|
|
731
|
+
[{"role": "user", "content": "Check this"}],
|
|
732
|
+
streaming=True # Enable streaming
|
|
733
|
+
),
|
|
734
|
+
])
|
|
735
|
+
"""
|
|
736
|
+
return AgentRunConfig(
|
|
737
|
+
agent=self,
|
|
738
|
+
input=input,
|
|
739
|
+
session_id=session_id,
|
|
740
|
+
conversation_id=conversation_id,
|
|
741
|
+
user_id=user_id,
|
|
742
|
+
streaming=streaming,
|
|
743
|
+
initial_state=initial_state,
|
|
744
|
+
run_timeout_seconds=run_timeout_seconds,
|
|
745
|
+
**kwargs,
|
|
746
|
+
)
|