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,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)