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
@@ -0,0 +1,121 @@
1
+ """Utility functions for conversation history management."""
2
+
3
+ from typing import Any
4
+ from urllib.parse import quote
5
+
6
+ import httpx
7
+
8
+ from ..utils.client_context import get_client_or_raise
9
+ from ..utils.worker_singleton import get_worker_client
10
+
11
+
12
+ async def add_conversation_history(
13
+ ctx: Any, # WorkflowContext
14
+ conversation_id: str,
15
+ agent_id: str,
16
+ role: str,
17
+ content: Any,
18
+ agent_run_id: str | None = None,
19
+ conversation_history_limit: int = 10,
20
+ ) -> None:
21
+ """Add a message to conversation history with durable execution.
22
+
23
+ Args:
24
+ ctx: WorkflowContext for durable execution
25
+ conversation_id: Conversation identifier
26
+ agent_id: Agent identifier
27
+ role: Message role ("user" or "assistant")
28
+ content: Message content (will be JSON-serialized)
29
+ agent_run_id: Optional agent run ID for traceability
30
+ conversation_history_limit: Maximum number of messages to keep (default: 10)
31
+ """
32
+ # Content should be JSON-serializable (string, dict, list, etc.)
33
+ # Pass content as-is - httpx will serialize it to JSON
34
+ content_json = content
35
+
36
+ polos_client = get_client_or_raise()
37
+ api_url = polos_client.api_url
38
+ headers = polos_client._get_headers()
39
+
40
+ request_json = {
41
+ "agent_id": agent_id,
42
+ "role": role,
43
+ "content": content_json,
44
+ "conversation_history_limit": conversation_history_limit,
45
+ }
46
+ if agent_run_id:
47
+ request_json["agent_run_id"] = agent_run_id
48
+
49
+ # Try to reuse worker's HTTP client if available
50
+ worker_client = get_worker_client()
51
+
52
+ encoded_conversation_id = quote(conversation_id, safe="")
53
+ if worker_client is not None:
54
+ response = await worker_client.post(
55
+ f"{api_url}/internal/conversation/{encoded_conversation_id}/add",
56
+ json=request_json,
57
+ headers=headers,
58
+ )
59
+ response.raise_for_status()
60
+ else:
61
+ async with httpx.AsyncClient() as client:
62
+ response = await client.post(
63
+ f"{api_url}/internal/conversation/{encoded_conversation_id}/add",
64
+ json=request_json,
65
+ headers=headers,
66
+ )
67
+ response.raise_for_status()
68
+
69
+
70
+ async def get_conversation_history(
71
+ conversation_id: str,
72
+ agent_id: str,
73
+ deployment_id: str | None = None,
74
+ limit: int | None = None,
75
+ ) -> list[dict[str, Any]]:
76
+ """Get conversation history for a conversation.
77
+
78
+ Args:
79
+ conversation_id: Conversation identifier
80
+ agent_id: Agent identifier (required)
81
+ deployment_id: Optional deployment identifier
82
+ limit: Optional limit on number of messages to return
83
+
84
+ Returns:
85
+ List of conversation messages (oldest first)
86
+ """
87
+ polos_client = get_client_or_raise()
88
+ api_url = polos_client.api_url
89
+ headers = polos_client._get_headers()
90
+
91
+ params = {
92
+ "agent_id": agent_id,
93
+ }
94
+ if deployment_id is not None:
95
+ params["deployment_id"] = deployment_id
96
+ if limit is not None:
97
+ params["limit"] = limit
98
+
99
+ # Try to reuse worker's HTTP client if available
100
+ worker_client = get_worker_client()
101
+
102
+ encoded_conversation_id = quote(conversation_id, safe="")
103
+ if worker_client is not None:
104
+ response = await worker_client.get(
105
+ f"{api_url}/api/v1/conversation/{encoded_conversation_id}/get",
106
+ params=params,
107
+ headers=headers,
108
+ )
109
+ response.raise_for_status()
110
+ result = response.json()
111
+ return result.get("messages", [])
112
+ else:
113
+ async with httpx.AsyncClient() as client:
114
+ response = await client.get(
115
+ f"{api_url}/api/v1/conversation/{encoded_conversation_id}/get",
116
+ params=params,
117
+ headers=headers,
118
+ )
119
+ response.raise_for_status()
120
+ result = response.json()
121
+ return result.get("messages", [])
@@ -0,0 +1,280 @@
1
+ """Stop conditions for agents.
2
+
3
+ Stop conditions allow you to define when an agent should stop executing.
4
+ Stop conditions execute durably within workflow context using step.run().
5
+ """
6
+
7
+ import logging
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from ..types.types import Step
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StopConditionContext(BaseModel):
19
+ """Context available to stop conditions.
20
+
21
+ This context is passed to stop conditions and contains information about
22
+ the current execution state.
23
+ """
24
+
25
+ # Steps executed so far
26
+ steps: list[Step] = []
27
+
28
+ # Optional agent context
29
+ agent_id: str | None = None
30
+ agent_run_id: str | None = None
31
+
32
+ def to_dict(self) -> dict[str, Any]:
33
+ """Convert stop condition context to dictionary for serialization."""
34
+ return self.model_dump(mode="json")
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: Any) -> "StopConditionContext":
38
+ """Create StopConditionContext from dictionary."""
39
+ if isinstance(data, StopConditionContext):
40
+ return data
41
+ if isinstance(data, dict):
42
+ return cls.model_validate(data)
43
+ raise TypeError(f"Cannot create StopConditionContext from {type(data)}")
44
+
45
+
46
+ def stop_condition(fn: Callable) -> Callable:
47
+ """
48
+ Decorator for stop condition functions.
49
+
50
+ Stop conditions take `ctx: StopConditionContext` as the first parameter
51
+ and optionally a second parameter (config class instance) for configuration.
52
+ They return a boolean (True to stop, False to continue).
53
+
54
+ When called with config params, they return a configured callable that will
55
+ be called later by the executor with StopConditionContext.
56
+
57
+ Usage:
58
+ # Simple stop condition (no config)
59
+ @stop_condition
60
+ async def always_stop(ctx: StopConditionContext) -> bool:
61
+ return True
62
+
63
+ # Stop condition with config
64
+ class MaxTokensConfig(BaseModel):
65
+ limit: int
66
+
67
+ @stop_condition
68
+ async def max_tokens(ctx: StopConditionContext, config: MaxTokensConfig) -> bool:
69
+ total = sum(step.usage.total_tokens if step.usage else 0 for step in ctx.steps)
70
+ return total >= config.limit
71
+
72
+ # Configure with limit
73
+ agent = Agent(..., stop_conditions=[max_tokens(MaxTokensConfig(limit=1000))])
74
+ """
75
+ import functools
76
+ import inspect
77
+
78
+ sig = inspect.signature(fn)
79
+ params = list(sig.parameters.values())
80
+
81
+ # Validate signature: first parameter must be StopConditionContext
82
+ if not params or params[0].annotation != StopConditionContext:
83
+ raise TypeError(
84
+ f"Invalid stop_condition function '{fn.__name__}': "
85
+ f"first parameter must be typed as 'StopConditionContext'. "
86
+ f"Got {params[0].annotation if params else 'no parameters'}."
87
+ )
88
+
89
+ # Check if there's a second parameter (for config) - it's optional
90
+ config_class = None
91
+ has_config = len(params) >= 2 and params[1].annotation != inspect.Signature.empty
92
+ if has_config:
93
+ config_class = params[1].annotation
94
+
95
+ # Validate that it's a Pydantic BaseModel
96
+ if not issubclass(config_class, BaseModel):
97
+ raise TypeError(
98
+ f"Invalid stop_condition function '{fn.__name__}': "
99
+ f"second parameter must be a Pydantic BaseModel class, got {config_class}."
100
+ )
101
+
102
+ # Check return type annotation (should be bool)
103
+ return_annotation = sig.return_annotation
104
+ if return_annotation != inspect.Signature.empty and return_annotation not in (bool, "bool"):
105
+ import asyncio
106
+ from typing import get_args
107
+
108
+ # Check for Coroutine[Any, Any, bool]
109
+ is_async_bool = False
110
+ if hasattr(return_annotation, "__origin__"):
111
+ origin = return_annotation.__origin__
112
+ if origin is asyncio.Coroutine:
113
+ args = get_args(return_annotation)
114
+ if len(args) >= 3 and args[2] is bool:
115
+ is_async_bool = True
116
+ if not is_async_bool:
117
+ logger.warning(
118
+ "stop_condition '%s' should return bool or Coroutine[..., bool], got %s",
119
+ fn.__name__,
120
+ return_annotation,
121
+ )
122
+
123
+ # Check if function is async
124
+ import asyncio
125
+
126
+ is_async = asyncio.iscoroutinefunction(fn)
127
+
128
+ @functools.wraps(fn)
129
+ def wrapper(*args, **kwargs):
130
+ """
131
+ Wrapper that returns a configured callable when called with config params.
132
+ The configured callable will be called later by the executor with StopConditionContext.
133
+ """
134
+ if has_config:
135
+ # Function requires config, so when called with args/kwargs, return configured callable
136
+ # Create config instance from captured args/kwargs
137
+ if args and len(args) == 1 and isinstance(args[0], config_class):
138
+ # Already an instance of config_class
139
+ config = args[0]
140
+ elif args and len(args) == 1 and isinstance(args[0], dict):
141
+ # Dict provided, use Pydantic's model_validate
142
+ config = config_class.model_validate(args[0])
143
+ elif kwargs:
144
+ # Keyword arguments provided, Pydantic constructor handles this
145
+ config = config_class(**kwargs)
146
+ elif args:
147
+ # Positional arguments provided, Pydantic constructor handles this
148
+ config = config_class(*args)
149
+ else:
150
+ raise ValueError(
151
+ f"stop_condition '{fn.__name__}' requires config but none provided"
152
+ )
153
+
154
+ # Create configured callable that captures config
155
+ # Preserve async/sync behavior of original function
156
+ if is_async:
157
+
158
+ async def configured_callable(ctx: StopConditionContext) -> bool:
159
+ """Async configured callable that executes the original function
160
+ with captured config."""
161
+ return await fn(ctx, config)
162
+ else:
163
+
164
+ def configured_callable(ctx: StopConditionContext) -> bool:
165
+ """Sync configured callable that executes the original function
166
+ with captured config."""
167
+ return fn(ctx, config)
168
+
169
+ # Add metadata for identification
170
+ configured_callable.__stop_condition_fn__ = fn
171
+ configured_callable.__stop_condition_name__ = fn.__name__
172
+ configured_callable.__stop_condition_config__ = config
173
+
174
+ return configured_callable
175
+ else:
176
+ # No config needed - the function itself can be called with ctx
177
+ # step.run() will handle it correctly
178
+ return fn
179
+
180
+ # Add metadata for identification
181
+ wrapper.__stop_condition_fn__ = fn
182
+ wrapper.__stop_condition_name__ = fn.__name__
183
+ wrapper.__stop_condition_has_config__ = has_config
184
+ wrapper.__stop_condition_config_class__ = config_class
185
+
186
+ return wrapper
187
+
188
+
189
+ # Built-in stop condition functions
190
+ class MaxTokensConfig(BaseModel):
191
+ """Configuration for max_tokens stop condition."""
192
+
193
+ limit: int
194
+
195
+
196
+ @stop_condition
197
+ async def max_tokens(ctx: StopConditionContext, config: MaxTokensConfig) -> bool:
198
+ """
199
+ Stop when total tokens exceed limit.
200
+
201
+ Usage:
202
+ agent = Agent(..., stop_conditions=[max_tokens(MaxTokensConfig(limit=1000))])
203
+ """
204
+ total = 0
205
+ for step in ctx.steps:
206
+ if step.usage:
207
+ total += step.usage.total_tokens
208
+ return total >= config.limit
209
+
210
+
211
+ class MaxStepsConfig(BaseModel):
212
+ """Configuration for max_steps stop condition."""
213
+
214
+ count: int = 5
215
+
216
+
217
+ @stop_condition
218
+ def max_steps(ctx: StopConditionContext, config: MaxStepsConfig) -> bool:
219
+ """
220
+ Stop when number of steps reaches count.
221
+
222
+ Usage:
223
+ agent = Agent(..., stop_conditions=[max_steps()]) # Uses default count=5
224
+ agent = Agent(..., stop_conditions=[max_steps(MaxStepsConfig(count=10))]) # Custom count
225
+ """
226
+ return len(ctx.steps) >= config.count
227
+
228
+
229
+ class ExecutedToolConfig(BaseModel):
230
+ """Configuration for executed_tool stop condition."""
231
+
232
+ tool_names: list[str]
233
+
234
+
235
+ @stop_condition
236
+ def executed_tool(ctx: StopConditionContext, config: ExecutedToolConfig) -> bool:
237
+ """
238
+ Stop when all specified tools have been executed.
239
+
240
+ Usage:
241
+ agent = Agent(..., stop_conditions=[
242
+ executed_tool(ExecutedToolConfig(tool_names=["get_weather", "search"]))
243
+ ])
244
+ """
245
+ required = set(config.tool_names)
246
+ if not required:
247
+ return False
248
+
249
+ executed = set()
250
+ for step in ctx.steps:
251
+ for tool_call in step.tool_calls:
252
+ executed.add(tool_call.function.name)
253
+ return required.issubset(executed)
254
+
255
+
256
+ class HasTextConfig(BaseModel):
257
+ """Configuration for has_text stop condition."""
258
+
259
+ texts: list[str]
260
+
261
+
262
+ @stop_condition
263
+ def has_text(ctx: StopConditionContext, config: HasTextConfig) -> bool:
264
+ """
265
+ Stop when all specified texts are found in response.
266
+
267
+ Usage:
268
+ agent = Agent(..., stop_conditions=[has_text(HasTextConfig(texts=["done", "complete"]))])
269
+ """
270
+ if not config.texts:
271
+ return False
272
+
273
+ # Concatenate all content strings from steps
274
+ combined = []
275
+ for step in ctx.steps:
276
+ if step.content:
277
+ combined.append(step.content)
278
+ full_text = " ".join(combined)
279
+
280
+ return all(t in full_text for t in config.texts)