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/middleware/hook.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Hook decorator for lifecycle hooks.
|
|
2
|
+
|
|
3
|
+
Hooks are callables that can intercept and modify execution at various lifecycle points.
|
|
4
|
+
They are executed within a workflow execution context and support durable execution.
|
|
5
|
+
|
|
6
|
+
Hooks have a specific signature: (ctx: WorkflowContext, hook_context: HookContext) -> HookResult
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict
|
|
15
|
+
|
|
16
|
+
from ..types.types import AgentConfig, Step
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HookAction(Enum):
|
|
20
|
+
"""Action a hook can take after execution."""
|
|
21
|
+
|
|
22
|
+
CONTINUE = "continue"
|
|
23
|
+
FAIL = "fail"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HookContext(BaseModel):
|
|
27
|
+
"""Context available to hooks.
|
|
28
|
+
|
|
29
|
+
This context is passed to hooks and contains information about
|
|
30
|
+
the current execution state.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Immutable identifiers
|
|
34
|
+
workflow_id: str
|
|
35
|
+
session_id: str | None = None
|
|
36
|
+
user_id: str | None = None
|
|
37
|
+
agent_config: AgentConfig | None = None # Not available to agent's on_start and on_end hooks
|
|
38
|
+
|
|
39
|
+
# Current state
|
|
40
|
+
steps: list[Step] = [] # All previous steps
|
|
41
|
+
|
|
42
|
+
# For workflow/tool hooks
|
|
43
|
+
current_tool: str | None = None
|
|
44
|
+
current_payload: dict[str, Any] | BaseModel | None = None
|
|
45
|
+
current_output: dict[str, Any] | BaseModel | None = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
"""Convert hook context to dictionary for serialization."""
|
|
49
|
+
return self.model_dump(mode="json")
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, data: Any) -> "HookContext":
|
|
53
|
+
"""Create HookContext from dictionary."""
|
|
54
|
+
if isinstance(data, HookContext):
|
|
55
|
+
return data
|
|
56
|
+
if isinstance(data, dict):
|
|
57
|
+
return cls.model_validate(data)
|
|
58
|
+
raise TypeError(f"Cannot create HookContext from {type(data)}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HookResult(BaseModel):
|
|
62
|
+
"""Result from a hook execution.
|
|
63
|
+
|
|
64
|
+
Hooks return this to indicate what action to take and any modifications
|
|
65
|
+
to apply to the execution state.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
69
|
+
|
|
70
|
+
action: HookAction = HookAction.CONTINUE
|
|
71
|
+
|
|
72
|
+
# Optional modifications
|
|
73
|
+
modified_payload: dict[str, Any] | None = None
|
|
74
|
+
modified_output: Any | None = None
|
|
75
|
+
|
|
76
|
+
# For FAIL action
|
|
77
|
+
error_message: str | None = None
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def continue_with(cls, **modifications) -> "HookResult":
|
|
81
|
+
"""Continue with optional modifications.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
**modifications: Can include modified_payload, modified_output
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
HookResult with CONTINUE action and modifications
|
|
88
|
+
"""
|
|
89
|
+
return cls(action=HookAction.CONTINUE, **modifications)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def fail(cls, message: str) -> "HookResult":
|
|
93
|
+
"""Fail processing with an error message.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
message: Error message to return
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
HookResult with FAIL action
|
|
100
|
+
"""
|
|
101
|
+
return cls(action=HookAction.FAIL, error_message=message)
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict[str, Any]:
|
|
104
|
+
"""Convert hook result to dictionary for serialization."""
|
|
105
|
+
return self.model_dump(mode="json")
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data: dict[str, Any]) -> "HookResult":
|
|
109
|
+
"""Create HookResult from dictionary."""
|
|
110
|
+
return cls.model_validate(data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_hook_signature(func: Callable) -> None:
|
|
114
|
+
"""Validate that hook function has correct signature.
|
|
115
|
+
|
|
116
|
+
Expected: (ctx: WorkflowContext, hook_context: HookContext) -> HookResult
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TypeError: If signature is invalid
|
|
120
|
+
"""
|
|
121
|
+
sig = inspect.signature(func)
|
|
122
|
+
params = list(sig.parameters.values())
|
|
123
|
+
|
|
124
|
+
# Must have exactly 2 parameters: ctx and hook_context
|
|
125
|
+
if len(params) != 2:
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"Hook function '{func.__name__}' must have exactly 2 parameters: "
|
|
128
|
+
f"(ctx: WorkflowContext, hook_context: HookContext). Got {len(params)} parameters."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def hook(func: Callable | None = None):
|
|
133
|
+
"""
|
|
134
|
+
Decorator to mark a function as a hook.
|
|
135
|
+
|
|
136
|
+
Hook functions must have the signature:
|
|
137
|
+
(ctx: WorkflowContext, hook_context: HookContext) -> HookResult
|
|
138
|
+
|
|
139
|
+
Usage:
|
|
140
|
+
@hook
|
|
141
|
+
def my_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
|
|
142
|
+
return HookResult.continue_with()
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
func: The function to decorate (when used as @hook)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The function itself (validated)
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
TypeError: If function signature is invalid
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def decorator(f: Callable) -> Callable:
|
|
155
|
+
# Validate function signature
|
|
156
|
+
_validate_hook_signature(f)
|
|
157
|
+
return f
|
|
158
|
+
|
|
159
|
+
# Handle @hook (without parentheses) - the function is passed as the first argument
|
|
160
|
+
if func is not None:
|
|
161
|
+
return decorator(func)
|
|
162
|
+
|
|
163
|
+
# Handle @hook() - return decorator
|
|
164
|
+
return decorator
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Hook execution infrastructure for lifecycle hooks.
|
|
2
|
+
|
|
3
|
+
Hooks are executed within a workflow execution context and support durable execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from ..core.context import WorkflowContext
|
|
9
|
+
from ..core.workflow import _execution_context
|
|
10
|
+
from .hook import HookAction, HookContext, HookResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_function_identifier(func: Callable, index: int) -> str:
|
|
14
|
+
"""Get a unique identifier for a function call.
|
|
15
|
+
|
|
16
|
+
Uses function name if available, otherwise falls back to index.
|
|
17
|
+
"""
|
|
18
|
+
if hasattr(func, "__name__") and func.__name__ != "<lambda>":
|
|
19
|
+
return func.__name__
|
|
20
|
+
return f"hook_{index}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def execute_hooks(
|
|
24
|
+
hook_name: str,
|
|
25
|
+
hooks: list[Callable],
|
|
26
|
+
hook_context: HookContext,
|
|
27
|
+
ctx: WorkflowContext,
|
|
28
|
+
) -> HookResult:
|
|
29
|
+
"""
|
|
30
|
+
Execute a list of hooks sequentially and return the combined result.
|
|
31
|
+
|
|
32
|
+
Hooks are executed within a workflow execution context. Each hook execution:
|
|
33
|
+
1. Checks for cached result (for durable execution)
|
|
34
|
+
2. If cached, returns cached result
|
|
35
|
+
3. If not cached, executes hook and stores result
|
|
36
|
+
|
|
37
|
+
Each hook can:
|
|
38
|
+
- Return CONTINUE to proceed to the next hook
|
|
39
|
+
- Return STOP to stop execution and return a value
|
|
40
|
+
- Return ERROR to stop execution with an error
|
|
41
|
+
|
|
42
|
+
Modifications from hooks are accumulated and applied in order.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
hooks: List of hook callables (functions decorated with @hook)
|
|
46
|
+
hook_context: Context to pass to hooks
|
|
47
|
+
ctx: WorkflowContext for the current execution
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
HookResult with action and any modifications
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If not executed within a workflow execution context
|
|
54
|
+
"""
|
|
55
|
+
if not hooks:
|
|
56
|
+
return HookResult.continue_with()
|
|
57
|
+
|
|
58
|
+
# Check we're in a workflow execution context
|
|
59
|
+
exec_context = _execution_context.get()
|
|
60
|
+
if not exec_context or not exec_context.get("execution_id"):
|
|
61
|
+
raise ValueError("Hooks must be executed within a workflow or agent")
|
|
62
|
+
|
|
63
|
+
# Accumulated modifications
|
|
64
|
+
modified_payload = hook_context.current_payload.copy() if hook_context.current_payload else {}
|
|
65
|
+
modified_output = hook_context.current_output.copy() if hook_context.current_output else {}
|
|
66
|
+
|
|
67
|
+
# Execute hooks sequentially
|
|
68
|
+
for index, hook_func in enumerate(hooks):
|
|
69
|
+
# Get function identifier for durable execution
|
|
70
|
+
func_id = _get_function_identifier(hook_func, index)
|
|
71
|
+
hook_result = await ctx.step.run(
|
|
72
|
+
f"{hook_name}.{func_id}.{index}", hook_func, ctx, hook_context
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Ensure result is HookResult
|
|
76
|
+
if not isinstance(hook_result, HookResult):
|
|
77
|
+
hook_result = HookResult.fail(
|
|
78
|
+
f"Hook '{func_id}' returned invalid result type: "
|
|
79
|
+
f"{type(hook_result)}. Expected HookResult."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Apply modifications
|
|
83
|
+
if hook_result.modified_payload is not None:
|
|
84
|
+
modified_payload.update(hook_result.modified_payload)
|
|
85
|
+
|
|
86
|
+
if hook_result.modified_output is not None:
|
|
87
|
+
modified_output.update(hook_result.modified_output)
|
|
88
|
+
|
|
89
|
+
# Update hook_context with accumulated modifications for next hook
|
|
90
|
+
hook_context.current_payload = modified_payload
|
|
91
|
+
hook_context.current_output = modified_output
|
|
92
|
+
|
|
93
|
+
# Check action
|
|
94
|
+
if hook_result.action == HookAction.FAIL:
|
|
95
|
+
# Fail execution - return error
|
|
96
|
+
return hook_result
|
|
97
|
+
|
|
98
|
+
# CONTINUE - proceed to next hook
|
|
99
|
+
|
|
100
|
+
# All hooks completed with CONTINUE - return accumulated modifications
|
|
101
|
+
return HookResult.continue_with(
|
|
102
|
+
modified_payload=modified_payload,
|
|
103
|
+
modified_output=modified_output,
|
|
104
|
+
)
|
|
File without changes
|
polos/runtime/batch.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Batch workflow triggering utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..agents.agent import AgentRunConfig
|
|
6
|
+
from ..core.workflow import _execution_context
|
|
7
|
+
from ..types.types import BatchWorkflowInput
|
|
8
|
+
from .client import ExecutionHandle, PolosClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def batch_invoke(
|
|
12
|
+
client: PolosClient,
|
|
13
|
+
workflows: list[BatchWorkflowInput],
|
|
14
|
+
session_id: str | None = None,
|
|
15
|
+
user_id: str | None = None,
|
|
16
|
+
) -> list[ExecutionHandle]:
|
|
17
|
+
"""Invoke multiple different workflows in a single batch and return handles immediately.
|
|
18
|
+
|
|
19
|
+
This function cannot be called from within a workflow or agent.
|
|
20
|
+
Use step.batch_invoke() to call workflows from within workflows.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
client: PolosClient instance
|
|
24
|
+
workflows: List of BatchWorkflowInput objects with 'id' (workflow_id string)
|
|
25
|
+
and 'payload' (dict or Pydantic model)
|
|
26
|
+
session_id: Optional session ID
|
|
27
|
+
user_id: Optional user ID
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of ExecutionHandle objects for the submitted workflows
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
handles = await batch_invoke([
|
|
34
|
+
BatchWorkflowInput(id="workflow-1", payload={"foo": "bar"}),
|
|
35
|
+
BatchWorkflowInput(id="workflow-2", payload={"baz": 42}),
|
|
36
|
+
])
|
|
37
|
+
"""
|
|
38
|
+
# Check if we're in an execution context - fail if we are
|
|
39
|
+
if _execution_context.get() is not None:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"batch_invoke() cannot be called from within a workflow or agent. "
|
|
42
|
+
"Use step.batch_invoke() to call workflows from within workflows."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return await client.batch_invoke(workflows, session_id=session_id, user_id=user_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def batch_agent_invoke(
|
|
49
|
+
client: PolosClient,
|
|
50
|
+
agents: list[AgentRunConfig],
|
|
51
|
+
) -> list[ExecutionHandle]:
|
|
52
|
+
"""
|
|
53
|
+
Invoke multiple agents in parallel and return execution handles.
|
|
54
|
+
|
|
55
|
+
This helper is intended for use with Agent.with_input(), which returns
|
|
56
|
+
AgentRunConfig instances.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
client: PolosClient instance
|
|
60
|
+
agents: List of AgentRunConfig instances
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
handles = await batch_agent_invoke([
|
|
64
|
+
grammar_agent.with_input("Check this"),
|
|
65
|
+
tone_agent.with_input("Check this too"),
|
|
66
|
+
])
|
|
67
|
+
"""
|
|
68
|
+
workflows: list[BatchWorkflowInput] = []
|
|
69
|
+
for config in agents:
|
|
70
|
+
payload: dict[str, Any] = {
|
|
71
|
+
"input": config.input,
|
|
72
|
+
"streaming": config.streaming,
|
|
73
|
+
"session_id": config.session_id,
|
|
74
|
+
"conversation_id": config.conversation_id,
|
|
75
|
+
"user_id": config.user_id,
|
|
76
|
+
**config.kwargs,
|
|
77
|
+
}
|
|
78
|
+
workflows.append(
|
|
79
|
+
BatchWorkflowInput(
|
|
80
|
+
id=config.agent.id,
|
|
81
|
+
payload=payload,
|
|
82
|
+
initial_state=config.initial_state,
|
|
83
|
+
run_timeout_seconds=config.run_timeout_seconds,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return await batch_invoke(client, workflows)
|