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.
Files changed (55) hide show
  1. polos/__init__.py +105 -0
  2. polos/agents/__init__.py +7 -0
  3. polos/agents/agent.py +746 -0
  4. polos/agents/conversation_history.py +121 -0
  5. polos/agents/stop_conditions.py +280 -0
  6. polos/agents/stream.py +635 -0
  7. polos/core/__init__.py +0 -0
  8. polos/core/context.py +143 -0
  9. polos/core/state.py +26 -0
  10. polos/core/step.py +1380 -0
  11. polos/core/workflow.py +1192 -0
  12. polos/features/__init__.py +0 -0
  13. polos/features/events.py +456 -0
  14. polos/features/schedules.py +110 -0
  15. polos/features/tracing.py +605 -0
  16. polos/features/wait.py +82 -0
  17. polos/llm/__init__.py +9 -0
  18. polos/llm/generate.py +152 -0
  19. polos/llm/providers/__init__.py +5 -0
  20. polos/llm/providers/anthropic.py +615 -0
  21. polos/llm/providers/azure.py +42 -0
  22. polos/llm/providers/base.py +196 -0
  23. polos/llm/providers/fireworks.py +41 -0
  24. polos/llm/providers/gemini.py +40 -0
  25. polos/llm/providers/groq.py +40 -0
  26. polos/llm/providers/openai.py +1021 -0
  27. polos/llm/providers/together.py +40 -0
  28. polos/llm/stream.py +183 -0
  29. polos/middleware/__init__.py +0 -0
  30. polos/middleware/guardrail.py +148 -0
  31. polos/middleware/guardrail_executor.py +253 -0
  32. polos/middleware/hook.py +164 -0
  33. polos/middleware/hook_executor.py +104 -0
  34. polos/runtime/__init__.py +0 -0
  35. polos/runtime/batch.py +87 -0
  36. polos/runtime/client.py +841 -0
  37. polos/runtime/queue.py +42 -0
  38. polos/runtime/worker.py +1365 -0
  39. polos/runtime/worker_server.py +249 -0
  40. polos/tools/__init__.py +0 -0
  41. polos/tools/tool.py +587 -0
  42. polos/types/__init__.py +23 -0
  43. polos/types/types.py +116 -0
  44. polos/utils/__init__.py +27 -0
  45. polos/utils/agent.py +27 -0
  46. polos/utils/client_context.py +41 -0
  47. polos/utils/config.py +12 -0
  48. polos/utils/output_schema.py +311 -0
  49. polos/utils/retry.py +47 -0
  50. polos/utils/serializer.py +167 -0
  51. polos/utils/tracing.py +27 -0
  52. polos/utils/worker_singleton.py +40 -0
  53. polos_sdk-0.1.0.dist-info/METADATA +650 -0
  54. polos_sdk-0.1.0.dist-info/RECORD +55 -0
  55. 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
+ )