modmex-ai 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.
- modmex_ai/__init__.py +155 -0
- modmex_ai/agents/__init__.py +16 -0
- modmex_ai/agents/agent.py +249 -0
- modmex_ai/agents/context.py +16 -0
- modmex_ai/agents/execution.py +172 -0
- modmex_ai/agents/handoff.py +117 -0
- modmex_ai/agents/result.py +23 -0
- modmex_ai/agents/stream.py +28 -0
- modmex_ai/approvals.py +35 -0
- modmex_ai/errors.py +107 -0
- modmex_ai/evals.py +84 -0
- modmex_ai/flows/__init__.py +15 -0
- modmex_ai/flows/continuation.py +12 -0
- modmex_ai/flows/flow.py +608 -0
- modmex_ai/flows/result.py +32 -0
- modmex_ai/flows/state.py +86 -0
- modmex_ai/flows/stream.py +22 -0
- modmex_ai/guardrails/__init__.py +27 -0
- modmex_ai/guardrails/guardrail.py +89 -0
- modmex_ai/guardrails/result.py +11 -0
- modmex_ai/http/__init__.py +6 -0
- modmex_ai/http/async_client.py +178 -0
- modmex_ai/http/client.py +234 -0
- modmex_ai/http/retries.py +14 -0
- modmex_ai/http/sse.py +29 -0
- modmex_ai/messages/__init__.py +4 -0
- modmex_ai/messages/message.py +17 -0
- modmex_ai/models/__init__.py +26 -0
- modmex_ai/models/base.py +29 -0
- modmex_ai/models/fake.py +39 -0
- modmex_ai/models/fallback.py +31 -0
- modmex_ai/models/profile.py +18 -0
- modmex_ai/models/provider_state.py +17 -0
- modmex_ai/models/request.py +22 -0
- modmex_ai/models/response.py +33 -0
- modmex_ai/models/settings.py +11 -0
- modmex_ai/models/stream.py +28 -0
- modmex_ai/models/usage.py +41 -0
- modmex_ai/observability.py +24 -0
- modmex_ai/providers/__init__.py +1 -0
- modmex_ai/providers/openai/__init__.py +26 -0
- modmex_ai/providers/openai/audio.py +161 -0
- modmex_ai/providers/openai/chat.py +86 -0
- modmex_ai/providers/openai/mapper.py +478 -0
- modmex_ai/providers/openai/profile.py +22 -0
- modmex_ai/providers/openai/realtime.py +409 -0
- modmex_ai/providers/openai/responses.py +99 -0
- modmex_ai/providers/openai/transcription.py +190 -0
- modmex_ai/providers/openai_compatible/__init__.py +4 -0
- modmex_ai/realtime/__init__.py +5 -0
- modmex_ai/realtime/event.py +13 -0
- modmex_ai/realtime/session.py +81 -0
- modmex_ai/realtime/transport.py +16 -0
- modmex_ai/schemas/__init__.py +13 -0
- modmex_ai/schemas/json_schema.py +110 -0
- modmex_ai/schemas/modmex.py +38 -0
- modmex_ai/sessions/__init__.py +7 -0
- modmex_ai/sessions/durable.py +38 -0
- modmex_ai/sessions/item.py +97 -0
- modmex_ai/sessions/memory.py +38 -0
- modmex_ai/sessions/session.py +28 -0
- modmex_ai/sessions/snapshot.py +38 -0
- modmex_ai/tools/__init__.py +4 -0
- modmex_ai/tools/executor.py +44 -0
- modmex_ai/tools/tool.py +215 -0
- modmex_ai/tracing/__init__.py +5 -0
- modmex_ai/tracing/step.py +27 -0
- modmex_ai/tracing/trace.py +26 -0
- modmex_ai/voice/__init__.py +49 -0
- modmex_ai/voice/chained.py +385 -0
- modmex_ai/voice/continuation.py +19 -0
- modmex_ai/voice/event.py +37 -0
- modmex_ai/voice/providers.py +129 -0
- modmex_ai/voice/session.py +21 -0
- modmex_ai/voice/turn.py +41 -0
- modmex_ai-0.1.0.dist-info/METADATA +305 -0
- modmex_ai-0.1.0.dist-info/RECORD +78 -0
- modmex_ai-0.1.0.dist-info/WHEEL +4 -0
modmex_ai/__init__.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Lightweight AI flows, agents, tools, and model adapters."""
|
|
2
|
+
|
|
3
|
+
from modmex_ai.agents import (
|
|
4
|
+
Agent,
|
|
5
|
+
AgentResult,
|
|
6
|
+
AgentStreamEvent,
|
|
7
|
+
AgentStreamEventType,
|
|
8
|
+
Handoff,
|
|
9
|
+
RECOMMENDED_PROMPT_PREFIX,
|
|
10
|
+
RunContext,
|
|
11
|
+
prompt_with_handoff_instructions,
|
|
12
|
+
)
|
|
13
|
+
from modmex_ai.models import (
|
|
14
|
+
AsyncModelClient,
|
|
15
|
+
FakeModel,
|
|
16
|
+
FallbackModel,
|
|
17
|
+
ModelClient,
|
|
18
|
+
ModelProfile,
|
|
19
|
+
ModelRequest,
|
|
20
|
+
ModelResponse,
|
|
21
|
+
ModelStreamEvent,
|
|
22
|
+
ModelStreamEventType,
|
|
23
|
+
ModelSettings,
|
|
24
|
+
ProviderState,
|
|
25
|
+
ToolCall,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
from modmex_ai.sessions import InMemorySession, Session, SessionItem, SessionSnapshot
|
|
29
|
+
from modmex_ai.sessions import DurableSessionStore, InMemoryDurableSessionStore, SessionConflictError
|
|
30
|
+
from modmex_ai.approvals import ApprovalDecision, ApprovalDecisionType, ApprovalPolicy, ApprovalRequest
|
|
31
|
+
from modmex_ai.observability import ObservabilityEvent, ObservabilityObserver
|
|
32
|
+
from modmex_ai.evals import EvalCase, EvalResult, EvalRunner
|
|
33
|
+
from modmex_ai.tools import Tool, ToolExecutor, ToolResult, as_tool, tool
|
|
34
|
+
from modmex_ai.flows import Flow, FlowContinuation, FlowResult, FlowStreamEvent, FlowStreamEventType, FlowStateConflictError, FlowStateStatus, FlowStateStore, FlowSuspended, FlowSuspension, InMemoryFlowStateStore, PersistedFlowState
|
|
35
|
+
from modmex_ai.guardrails import (
|
|
36
|
+
Guardrail,
|
|
37
|
+
GuardrailResult,
|
|
38
|
+
InputGuardrail,
|
|
39
|
+
OutputGuardrail,
|
|
40
|
+
ToolInputGuardrail,
|
|
41
|
+
ToolOutputGuardrail,
|
|
42
|
+
)
|
|
43
|
+
from modmex_ai.realtime import RealtimeEvent, RealtimeSession, RealtimeTransport
|
|
44
|
+
from modmex_ai.voice import (
|
|
45
|
+
ChainedVoiceSession,
|
|
46
|
+
CallableSpeechToTextProvider,
|
|
47
|
+
CallableTextToSpeechProvider,
|
|
48
|
+
LiveSpeechToTextProvider,
|
|
49
|
+
LiveSpeechToTextSession,
|
|
50
|
+
SpeechToTextProvider,
|
|
51
|
+
StreamingSpeechToTextProvider,
|
|
52
|
+
StreamingTextToSpeechProvider,
|
|
53
|
+
TextToSpeechProvider,
|
|
54
|
+
Transcription,
|
|
55
|
+
VoiceInputEvent,
|
|
56
|
+
VoiceInputEventType,
|
|
57
|
+
VoiceAgentSession,
|
|
58
|
+
VoiceSessionEvent,
|
|
59
|
+
VoiceSessionEventType,
|
|
60
|
+
VoiceContinuation,
|
|
61
|
+
VoiceTerminationReason,
|
|
62
|
+
VoiceTurnOptions,
|
|
63
|
+
VoiceTurnMetrics,
|
|
64
|
+
VoiceTurnOutcome,
|
|
65
|
+
VoiceTurnReason,
|
|
66
|
+
VoiceTurnObserver,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"Agent",
|
|
71
|
+
"AsyncModelClient",
|
|
72
|
+
"AgentResult",
|
|
73
|
+
"AgentStreamEvent",
|
|
74
|
+
"AgentStreamEventType",
|
|
75
|
+
"FakeModel",
|
|
76
|
+
"FallbackModel",
|
|
77
|
+
"InMemorySession",
|
|
78
|
+
"DurableSessionStore",
|
|
79
|
+
"InMemoryDurableSessionStore",
|
|
80
|
+
"SessionConflictError",
|
|
81
|
+
"ModelClient",
|
|
82
|
+
"ModelProfile",
|
|
83
|
+
"ModelRequest",
|
|
84
|
+
"ModelResponse",
|
|
85
|
+
"ModelStreamEvent",
|
|
86
|
+
"ModelStreamEventType",
|
|
87
|
+
"ModelSettings",
|
|
88
|
+
"ProviderState",
|
|
89
|
+
"RECOMMENDED_PROMPT_PREFIX",
|
|
90
|
+
"RunContext",
|
|
91
|
+
"Session",
|
|
92
|
+
"SessionItem",
|
|
93
|
+
"SessionSnapshot",
|
|
94
|
+
"ApprovalDecision",
|
|
95
|
+
"ApprovalDecisionType",
|
|
96
|
+
"ApprovalPolicy",
|
|
97
|
+
"ApprovalRequest",
|
|
98
|
+
"ObservabilityEvent",
|
|
99
|
+
"ObservabilityObserver",
|
|
100
|
+
"EvalCase",
|
|
101
|
+
"EvalResult",
|
|
102
|
+
"EvalRunner",
|
|
103
|
+
"Tool",
|
|
104
|
+
"ToolCall",
|
|
105
|
+
"ToolExecutor",
|
|
106
|
+
"ToolResult",
|
|
107
|
+
"Flow",
|
|
108
|
+
"FlowContinuation",
|
|
109
|
+
"FlowResult",
|
|
110
|
+
"FlowStreamEvent",
|
|
111
|
+
"FlowStreamEventType",
|
|
112
|
+
"FlowStateConflictError",
|
|
113
|
+
"FlowStateStatus",
|
|
114
|
+
"FlowStateStore",
|
|
115
|
+
"FlowSuspended",
|
|
116
|
+
"FlowSuspension",
|
|
117
|
+
"InMemoryFlowStateStore",
|
|
118
|
+
"PersistedFlowState",
|
|
119
|
+
"Handoff",
|
|
120
|
+
"Guardrail",
|
|
121
|
+
"GuardrailResult",
|
|
122
|
+
"InputGuardrail",
|
|
123
|
+
"OutputGuardrail",
|
|
124
|
+
"ToolInputGuardrail",
|
|
125
|
+
"ToolOutputGuardrail",
|
|
126
|
+
"Usage",
|
|
127
|
+
"ChainedVoiceSession",
|
|
128
|
+
"CallableSpeechToTextProvider",
|
|
129
|
+
"CallableTextToSpeechProvider",
|
|
130
|
+
"SpeechToTextProvider",
|
|
131
|
+
"LiveSpeechToTextProvider",
|
|
132
|
+
"LiveSpeechToTextSession",
|
|
133
|
+
"StreamingSpeechToTextProvider",
|
|
134
|
+
"StreamingTextToSpeechProvider",
|
|
135
|
+
"TextToSpeechProvider",
|
|
136
|
+
"Transcription",
|
|
137
|
+
"VoiceInputEvent",
|
|
138
|
+
"VoiceInputEventType",
|
|
139
|
+
"VoiceAgentSession",
|
|
140
|
+
"VoiceContinuation",
|
|
141
|
+
"VoiceSessionEvent",
|
|
142
|
+
"VoiceSessionEventType",
|
|
143
|
+
"VoiceTerminationReason",
|
|
144
|
+
"VoiceTurnOptions",
|
|
145
|
+
"VoiceTurnMetrics",
|
|
146
|
+
"VoiceTurnOutcome",
|
|
147
|
+
"VoiceTurnReason",
|
|
148
|
+
"VoiceTurnObserver",
|
|
149
|
+
"RealtimeEvent",
|
|
150
|
+
"RealtimeSession",
|
|
151
|
+
"RealtimeTransport",
|
|
152
|
+
"as_tool",
|
|
153
|
+
"prompt_with_handoff_instructions",
|
|
154
|
+
"tool",
|
|
155
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from modmex_ai.agents.agent import Agent
|
|
2
|
+
from modmex_ai.agents.context import RunContext
|
|
3
|
+
from modmex_ai.agents.handoff import Handoff, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
|
4
|
+
from modmex_ai.agents.result import AgentResult
|
|
5
|
+
from modmex_ai.agents.stream import AgentStreamEvent, AgentStreamEventType
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Agent",
|
|
9
|
+
"AgentResult",
|
|
10
|
+
"AgentStreamEvent",
|
|
11
|
+
"AgentStreamEventType",
|
|
12
|
+
"Handoff",
|
|
13
|
+
"RECOMMENDED_PROMPT_PREFIX",
|
|
14
|
+
"RunContext",
|
|
15
|
+
"prompt_with_handoff_instructions",
|
|
16
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from modmex_ai.agents.context import RunContext
|
|
8
|
+
from modmex_ai.agents.execution import AgentExecution
|
|
9
|
+
from modmex_ai.agents.handoff import Handoff, normalize_handoffs
|
|
10
|
+
from modmex_ai.agents.result import AgentResult
|
|
11
|
+
from modmex_ai.agents.stream import AgentStreamEvent, AgentStreamEventType
|
|
12
|
+
from modmex_ai.errors import OutputGuardrailTriggered, OutputValidationError
|
|
13
|
+
from modmex_ai.guardrails import (
|
|
14
|
+
InputGuardrail,
|
|
15
|
+
OutputGuardrail,
|
|
16
|
+
ToolInputGuardrail,
|
|
17
|
+
ToolOutputGuardrail,
|
|
18
|
+
enforce_input_guardrails,
|
|
19
|
+
enforce_output_guardrails,
|
|
20
|
+
)
|
|
21
|
+
from modmex_ai.messages import Message
|
|
22
|
+
from modmex_ai.models import (
|
|
23
|
+
ModelClient,
|
|
24
|
+
ModelResponse,
|
|
25
|
+
ModelSettings,
|
|
26
|
+
ModelStreamEvent,
|
|
27
|
+
ModelStreamEventType,
|
|
28
|
+
ProviderState,
|
|
29
|
+
Usage,
|
|
30
|
+
)
|
|
31
|
+
from modmex_ai.schemas import dumps, schema_for_model, validate_model
|
|
32
|
+
from modmex_ai.sessions import SessionItem
|
|
33
|
+
from modmex_ai.tools import Tool, as_tool, tool as create_tool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_NESTED_USAGE_STATE_KEY = "__modmex_ai_nested_usage"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Agent:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
name: str,
|
|
44
|
+
instructions: str,
|
|
45
|
+
output_type: type[Any] | None = None,
|
|
46
|
+
output_strict: bool = True,
|
|
47
|
+
tools: list[Tool | Any] | None = None,
|
|
48
|
+
handoffs: list[str | Handoff] | None = None,
|
|
49
|
+
model: ModelClient | None = None,
|
|
50
|
+
settings: ModelSettings | None = None,
|
|
51
|
+
max_tool_calls: int = 8,
|
|
52
|
+
input_guardrails: list[InputGuardrail] | None = None,
|
|
53
|
+
output_guardrails: list[OutputGuardrail] | None = None,
|
|
54
|
+
max_output_guardrail_retries: int = 0,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.name = name
|
|
57
|
+
self.instructions = instructions
|
|
58
|
+
self.output_type = output_type
|
|
59
|
+
self.output_strict = output_strict
|
|
60
|
+
self.tools = [as_tool(value) for value in (tools or [])]
|
|
61
|
+
self.handoffs = normalize_handoffs(handoffs)
|
|
62
|
+
self.model = model
|
|
63
|
+
self.settings = settings
|
|
64
|
+
self.max_tool_calls = max_tool_calls
|
|
65
|
+
self.input_guardrails = input_guardrails or []
|
|
66
|
+
self.output_guardrails = output_guardrails or []
|
|
67
|
+
if max_output_guardrail_retries < 0:
|
|
68
|
+
raise ValueError("max_output_guardrail_retries must be non-negative")
|
|
69
|
+
self.max_output_guardrail_retries = max_output_guardrail_retries
|
|
70
|
+
|
|
71
|
+
def add_tool(self, value: Tool | Any) -> Tool:
|
|
72
|
+
registered = as_tool(value)
|
|
73
|
+
self.tools.append(registered)
|
|
74
|
+
return registered
|
|
75
|
+
|
|
76
|
+
def tool(
|
|
77
|
+
self,
|
|
78
|
+
func: Any = None,
|
|
79
|
+
*,
|
|
80
|
+
name: str | None = None,
|
|
81
|
+
description: str | None = None,
|
|
82
|
+
input_guardrails: list[ToolInputGuardrail] | None = None,
|
|
83
|
+
output_guardrails: list[ToolOutputGuardrail] | None = None,
|
|
84
|
+
requires_approval: bool = False,
|
|
85
|
+
approval_reason: str | None = None,
|
|
86
|
+
):
|
|
87
|
+
def decorator(inner):
|
|
88
|
+
return self.add_tool(create_tool(
|
|
89
|
+
inner,
|
|
90
|
+
name=name,
|
|
91
|
+
description=description,
|
|
92
|
+
input_guardrails=input_guardrails,
|
|
93
|
+
output_guardrails=output_guardrails,
|
|
94
|
+
requires_approval=requires_approval,
|
|
95
|
+
approval_reason=approval_reason,
|
|
96
|
+
))
|
|
97
|
+
return decorator if func is None else decorator(func)
|
|
98
|
+
|
|
99
|
+
def as_tool(self, *, tool_name: str | None = None, tool_description: str | None = None) -> Tool:
|
|
100
|
+
def run_agent(input: str, ctx=None) -> Any:
|
|
101
|
+
result = self.run(input, context=ctx)
|
|
102
|
+
if isinstance(ctx, RunContext):
|
|
103
|
+
nested_usage = ctx.state.setdefault(_NESTED_USAGE_STATE_KEY, Usage())
|
|
104
|
+
nested_usage.add(result.usage)
|
|
105
|
+
return result.output
|
|
106
|
+
return create_tool(run_agent, name=tool_name or self.name, description=tool_description or self.instructions)
|
|
107
|
+
|
|
108
|
+
def run(self, input: Any, **kwargs: Any) -> AgentResult:
|
|
109
|
+
return self._run_execution(self._execution(input, **kwargs))
|
|
110
|
+
|
|
111
|
+
async def run_async(self, input: Any, **kwargs: Any) -> AgentResult:
|
|
112
|
+
execution = self._execution(input, **kwargs)
|
|
113
|
+
acomplete = getattr(execution.model, "acomplete", None)
|
|
114
|
+
if not callable(acomplete):
|
|
115
|
+
return await asyncio.to_thread(self._run_execution, execution)
|
|
116
|
+
while True:
|
|
117
|
+
step = execution.accept_response(await acomplete(execution.next_request()))
|
|
118
|
+
if step.result is not None:
|
|
119
|
+
return step.result
|
|
120
|
+
for tool_call in step.tool_calls or []:
|
|
121
|
+
execution.begin_tool_call(tool_call)
|
|
122
|
+
execution.accept_tool_result(await execution.executor.execute_async(tool_call, context=execution.context))
|
|
123
|
+
|
|
124
|
+
def run_stream(self, input: Any, **kwargs: Any):
|
|
125
|
+
execution = self._execution(input, **kwargs)
|
|
126
|
+
while True:
|
|
127
|
+
completed: ModelResponse | None = None
|
|
128
|
+
for raw_event in execution.model.stream(execution.next_request()):
|
|
129
|
+
event = self._model_stream_event(raw_event)
|
|
130
|
+
if event.type == ModelStreamEventType.TEXT_DELTA:
|
|
131
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TEXT_DELTA, text_delta=event.text_delta or "")
|
|
132
|
+
elif event.type == ModelStreamEventType.TOOL_CALL_DELTA:
|
|
133
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=event.tool_call)
|
|
134
|
+
elif event.type == ModelStreamEventType.COMPLETED:
|
|
135
|
+
completed = event.response
|
|
136
|
+
if completed is None:
|
|
137
|
+
raise RuntimeError("Model stream ended without a completed response")
|
|
138
|
+
step = execution.accept_response(completed)
|
|
139
|
+
if step.result is not None:
|
|
140
|
+
yield AgentStreamEvent(type=AgentStreamEventType.HANDOFF if step.result.handoff_target else AgentStreamEventType.COMPLETED, result=step.result)
|
|
141
|
+
return
|
|
142
|
+
for tool_call in step.tool_calls or []:
|
|
143
|
+
execution.begin_tool_call(tool_call)
|
|
144
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=tool_call)
|
|
145
|
+
tool_result = execution.executor.execute(tool_call, context=execution.context)
|
|
146
|
+
execution.accept_tool_result(tool_result)
|
|
147
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_FINISHED, tool_call=tool_call, data={"output": tool_result.output})
|
|
148
|
+
|
|
149
|
+
async def arun_stream(self, input: Any, **kwargs: Any):
|
|
150
|
+
"""Async counterpart of ``run_stream`` for models with native streaming."""
|
|
151
|
+
execution = self._execution(input, **kwargs)
|
|
152
|
+
astream = getattr(execution.model, "astream", None)
|
|
153
|
+
if not callable(astream):
|
|
154
|
+
raise NotImplementedError("Model does not implement native async streaming")
|
|
155
|
+
while True:
|
|
156
|
+
completed: ModelResponse | None = None
|
|
157
|
+
async for raw_event in astream(execution.next_request()):
|
|
158
|
+
event = self._model_stream_event(raw_event)
|
|
159
|
+
if event.type == ModelStreamEventType.TEXT_DELTA:
|
|
160
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TEXT_DELTA, text_delta=event.text_delta or "")
|
|
161
|
+
elif event.type == ModelStreamEventType.TOOL_CALL_DELTA:
|
|
162
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=event.tool_call)
|
|
163
|
+
elif event.type == ModelStreamEventType.COMPLETED:
|
|
164
|
+
completed = event.response
|
|
165
|
+
if completed is None:
|
|
166
|
+
raise RuntimeError("Model stream ended without a completed response")
|
|
167
|
+
step = execution.accept_response(completed)
|
|
168
|
+
if step.result is not None:
|
|
169
|
+
yield AgentStreamEvent(type=AgentStreamEventType.HANDOFF if step.result.handoff_target else AgentStreamEventType.COMPLETED, result=step.result)
|
|
170
|
+
return
|
|
171
|
+
for tool_call in step.tool_calls or []:
|
|
172
|
+
execution.begin_tool_call(tool_call)
|
|
173
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=tool_call)
|
|
174
|
+
tool_result = await execution.executor.execute_async(tool_call, context=execution.context)
|
|
175
|
+
execution.accept_tool_result(tool_result)
|
|
176
|
+
yield AgentStreamEvent(type=AgentStreamEventType.TOOL_FINISHED, tool_call=tool_call, data={"output": tool_result.output})
|
|
177
|
+
|
|
178
|
+
def _run_execution(self, execution: AgentExecution) -> AgentResult:
|
|
179
|
+
while True:
|
|
180
|
+
step = execution.accept_response(execution.model.complete(execution.next_request()))
|
|
181
|
+
if step.result is not None:
|
|
182
|
+
return step.result
|
|
183
|
+
for tool_call in step.tool_calls or []:
|
|
184
|
+
execution.begin_tool_call(tool_call)
|
|
185
|
+
execution.accept_tool_result(execution.executor.execute(tool_call, context=execution.context))
|
|
186
|
+
|
|
187
|
+
def _execution(
|
|
188
|
+
self,
|
|
189
|
+
input: Any,
|
|
190
|
+
*,
|
|
191
|
+
model: ModelClient | None = None,
|
|
192
|
+
context: RunContext | Any = None,
|
|
193
|
+
provider_state: ProviderState | None = None,
|
|
194
|
+
run_input_guardrails: bool = True,
|
|
195
|
+
run_output_guardrails: bool = True,
|
|
196
|
+
) -> AgentExecution:
|
|
197
|
+
active_model = model or self.model
|
|
198
|
+
if active_model is None:
|
|
199
|
+
raise ValueError(f"Agent {self.name!r} has no model")
|
|
200
|
+
run_context = context if isinstance(context, RunContext) else RunContext(input=input, context=context)
|
|
201
|
+
if run_input_guardrails:
|
|
202
|
+
enforce_input_guardrails(input, self.input_guardrails, context=run_context)
|
|
203
|
+
return AgentExecution(agent=self, model=active_model, input=input, context=run_context, provider_state=provider_state, run_output_guardrails=run_output_guardrails)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _model_stream_event(value: ModelStreamEvent | ModelResponse) -> ModelStreamEvent:
|
|
207
|
+
if isinstance(value, ModelResponse):
|
|
208
|
+
return ModelStreamEvent.completed(value)
|
|
209
|
+
if isinstance(value, ModelStreamEvent):
|
|
210
|
+
return value
|
|
211
|
+
raise TypeError("Model stream must yield ModelStreamEvent or ModelResponse")
|
|
212
|
+
|
|
213
|
+
def _output_schema(self) -> dict[str, Any] | None:
|
|
214
|
+
return schema_for_model(self.output_type) if self.output_type else None
|
|
215
|
+
|
|
216
|
+
def _should_retry_output(self, *, output: Any, response_text: str | None, messages: list[Message | SessionItem], context: RunContext, retry_count: int) -> bool:
|
|
217
|
+
try:
|
|
218
|
+
enforce_output_guardrails(output, self.output_guardrails, context=context)
|
|
219
|
+
except OutputGuardrailTriggered as error:
|
|
220
|
+
if retry_count >= self.max_output_guardrail_retries:
|
|
221
|
+
raise
|
|
222
|
+
context.trace.add("guardrail", f"{self.name}_output_retry", attempt=retry_count + 1, reason=str(error))
|
|
223
|
+
messages.extend([
|
|
224
|
+
Message(role="assistant", content=response_text or ""),
|
|
225
|
+
Message(role="developer", content="The previous candidate output was rejected by an output guardrail: " f"{error}. Produce a corrected replacement that fully satisfies the output schema and the guardrail rule."),
|
|
226
|
+
])
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def _initial_messages(self, input: Any, *, include_output_schema_prompt: bool = True) -> list[Message | SessionItem]:
|
|
231
|
+
instructions = self.instructions
|
|
232
|
+
if self.output_type and include_output_schema_prompt:
|
|
233
|
+
instructions += "\n\nReturn only JSON that matches this schema:\n" + json.dumps(schema_for_model(self.output_type), separators=(",", ":"))
|
|
234
|
+
return [Message(role="system", content=instructions), *self._input_messages(input)]
|
|
235
|
+
|
|
236
|
+
def _input_messages(self, input: Any) -> list[Message | SessionItem]:
|
|
237
|
+
if isinstance(input, list) and all(isinstance(item, (Message, SessionItem, dict)) for item in input):
|
|
238
|
+
return [item if isinstance(item, (Message, SessionItem)) else SessionItem(**item) if item.get("type") else Message(**item) for item in input]
|
|
239
|
+
return [Message(role="user", content=input if isinstance(input, str) else dumps(input))]
|
|
240
|
+
|
|
241
|
+
def _parse_output(self, text: str | None) -> Any:
|
|
242
|
+
if self.output_type is None:
|
|
243
|
+
return text or ""
|
|
244
|
+
if text is None:
|
|
245
|
+
raise OutputValidationError(f"Agent {self.name!r} returned no output")
|
|
246
|
+
return validate_model(json.loads(text), self.output_type)
|
|
247
|
+
|
|
248
|
+
def _handoff_schemas(self) -> list[dict[str, Any]]:
|
|
249
|
+
return [handoff.schema() for handoff in self.handoffs]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from modmex import BaseModel
|
|
7
|
+
|
|
8
|
+
from modmex_ai.tracing import Trace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunContext(BaseModel):
|
|
12
|
+
input: Any
|
|
13
|
+
context: Any = None
|
|
14
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
16
|
+
trace: Trace = field(default_factory=Trace)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from modmex_ai.agents.context import RunContext
|
|
7
|
+
from modmex_ai.agents.handoff import find_handoff
|
|
8
|
+
from modmex_ai.agents.result import AgentResult
|
|
9
|
+
from modmex_ai.errors import MaxToolCallsExceeded
|
|
10
|
+
from modmex_ai.messages import Message
|
|
11
|
+
from modmex_ai.models import ModelClient, ModelRequest, ModelResponse, ProviderState, ToolCall, Usage
|
|
12
|
+
from modmex_ai.sessions import SessionItem
|
|
13
|
+
from modmex_ai.tools import ToolExecutor, ToolResult
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from modmex_ai.agents.agent import Agent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AgentExecutionStep:
|
|
21
|
+
"""The next action requested by the provider-neutral agent state machine."""
|
|
22
|
+
|
|
23
|
+
result: AgentResult | None = None
|
|
24
|
+
tool_calls: list[ToolCall] | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentExecution:
|
|
28
|
+
"""Shared Agent state machine; sync and async drivers supply I/O only."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
agent: Agent,
|
|
34
|
+
model: ModelClient,
|
|
35
|
+
input: Any,
|
|
36
|
+
context: RunContext,
|
|
37
|
+
provider_state: ProviderState | None,
|
|
38
|
+
run_output_guardrails: bool,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.agent = agent
|
|
41
|
+
self.model = model
|
|
42
|
+
self.context = context
|
|
43
|
+
self.run_output_guardrails = run_output_guardrails
|
|
44
|
+
self.messages = agent._initial_messages(
|
|
45
|
+
input,
|
|
46
|
+
include_output_schema_prompt=not self.model.profile.supports_structured_output,
|
|
47
|
+
)
|
|
48
|
+
self.executor = ToolExecutor(agent.tools)
|
|
49
|
+
self.tool_schemas = [*self.executor.schemas(), *agent._handoff_schemas()]
|
|
50
|
+
self.output_schema = agent._output_schema()
|
|
51
|
+
self.tool_call_count = 0
|
|
52
|
+
self.output_guardrail_retry_count = 0
|
|
53
|
+
self.usage = Usage()
|
|
54
|
+
self.generated_items: list[SessionItem] = []
|
|
55
|
+
self.provider_state = provider_state
|
|
56
|
+
|
|
57
|
+
def next_request(self) -> ModelRequest:
|
|
58
|
+
self.context.trace.add(
|
|
59
|
+
"model_request",
|
|
60
|
+
self.agent.name,
|
|
61
|
+
tools=[tool["name"] for tool in self.tool_schemas],
|
|
62
|
+
)
|
|
63
|
+
return ModelRequest(
|
|
64
|
+
messages=self.messages,
|
|
65
|
+
tools=self.tool_schemas,
|
|
66
|
+
output_schema=self.output_schema,
|
|
67
|
+
output_strict=self.agent.output_strict,
|
|
68
|
+
settings=self.agent.settings,
|
|
69
|
+
provider_state=self.provider_state,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def accept_response(self, response: ModelResponse) -> AgentExecutionStep:
|
|
73
|
+
if response.provider_state is not None:
|
|
74
|
+
self.provider_state = response.provider_state
|
|
75
|
+
self.usage.add(response.usage)
|
|
76
|
+
self.context.trace.add(
|
|
77
|
+
"model_response",
|
|
78
|
+
self.agent.name,
|
|
79
|
+
request_id=response.request_id,
|
|
80
|
+
tool_calls=[call.name for call in response.tool_calls],
|
|
81
|
+
usage=response.usage,
|
|
82
|
+
)
|
|
83
|
+
if response.tool_calls:
|
|
84
|
+
return self._tool_step(response.tool_calls)
|
|
85
|
+
|
|
86
|
+
output = self.agent._parse_output(response.output_text)
|
|
87
|
+
if self.run_output_guardrails and self.agent._should_retry_output(
|
|
88
|
+
output=output,
|
|
89
|
+
response_text=response.output_text,
|
|
90
|
+
messages=self.messages,
|
|
91
|
+
context=self.context,
|
|
92
|
+
retry_count=self.output_guardrail_retry_count,
|
|
93
|
+
):
|
|
94
|
+
self.output_guardrail_retry_count += 1
|
|
95
|
+
return AgentExecutionStep()
|
|
96
|
+
return AgentExecutionStep(result=self._result(output=output))
|
|
97
|
+
|
|
98
|
+
def begin_tool_call(self, tool_call: ToolCall) -> None:
|
|
99
|
+
item = SessionItem(
|
|
100
|
+
type="function_call",
|
|
101
|
+
tool_call_id=tool_call.tool_call_id,
|
|
102
|
+
name=tool_call.name,
|
|
103
|
+
arguments=tool_call.arguments,
|
|
104
|
+
)
|
|
105
|
+
self.generated_items.append(item)
|
|
106
|
+
self.messages.append(item)
|
|
107
|
+
|
|
108
|
+
def accept_tool_result(self, result: ToolResult) -> None:
|
|
109
|
+
item = SessionItem(
|
|
110
|
+
type="function_call_output",
|
|
111
|
+
tool_call_id=result.tool_call_id,
|
|
112
|
+
name=result.name,
|
|
113
|
+
output=result.output,
|
|
114
|
+
)
|
|
115
|
+
self.generated_items.append(item)
|
|
116
|
+
nested_usage = self.context.state.pop("__modmex_ai_nested_usage", None)
|
|
117
|
+
if nested_usage:
|
|
118
|
+
self.usage.add(nested_usage)
|
|
119
|
+
self.context.trace.add("tool_call", result.name, tool_call_id=result.tool_call_id)
|
|
120
|
+
self.messages.append(item)
|
|
121
|
+
|
|
122
|
+
def _tool_step(self, tool_calls: list[ToolCall]) -> AgentExecutionStep:
|
|
123
|
+
self.tool_call_count += len(tool_calls)
|
|
124
|
+
if self.tool_call_count > self.agent.max_tool_calls:
|
|
125
|
+
raise MaxToolCallsExceeded(f"Agent {self.agent.name!r} exceeded max_tool_calls")
|
|
126
|
+
for tool_call in tool_calls:
|
|
127
|
+
handoff = find_handoff(tool_call, self.agent.handoffs)
|
|
128
|
+
if handoff is None:
|
|
129
|
+
continue
|
|
130
|
+
handoff_input = handoff.invoke(self.context, tool_call.arguments)
|
|
131
|
+
self.generated_items.extend([
|
|
132
|
+
SessionItem(
|
|
133
|
+
type="handoff_call",
|
|
134
|
+
tool_call_id=tool_call.tool_call_id,
|
|
135
|
+
name=handoff.name,
|
|
136
|
+
arguments=tool_call.arguments,
|
|
137
|
+
),
|
|
138
|
+
SessionItem(
|
|
139
|
+
type="handoff_call_output",
|
|
140
|
+
tool_call_id=tool_call.tool_call_id,
|
|
141
|
+
name=handoff.name,
|
|
142
|
+
output={"transferred": True},
|
|
143
|
+
),
|
|
144
|
+
])
|
|
145
|
+
self.context.trace.add(
|
|
146
|
+
"handoff_call",
|
|
147
|
+
handoff.name,
|
|
148
|
+
tool_call_id=tool_call.tool_call_id,
|
|
149
|
+
to=handoff.agent,
|
|
150
|
+
)
|
|
151
|
+
return AgentExecutionStep(result=AgentResult(
|
|
152
|
+
output=None,
|
|
153
|
+
agent=self.agent.name,
|
|
154
|
+
handoff_target=handoff.agent,
|
|
155
|
+
handoff_name=handoff.name,
|
|
156
|
+
handoff_input=handoff_input,
|
|
157
|
+
usage=self.usage,
|
|
158
|
+
items=self.generated_items,
|
|
159
|
+
provider_state=self.provider_state,
|
|
160
|
+
trace=self.context.trace,
|
|
161
|
+
))
|
|
162
|
+
return AgentExecutionStep(tool_calls=tool_calls)
|
|
163
|
+
|
|
164
|
+
def _result(self, *, output: Any) -> AgentResult:
|
|
165
|
+
return AgentResult(
|
|
166
|
+
output=output,
|
|
167
|
+
agent=self.agent.name,
|
|
168
|
+
usage=self.usage,
|
|
169
|
+
items=self.generated_items,
|
|
170
|
+
trace=self.context.trace,
|
|
171
|
+
provider_state=self.provider_state,
|
|
172
|
+
)
|