modmex-ai 0.1.0__tar.gz
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-0.1.0/.examples/.env.example +3 -0
- modmex_ai-0.1.0/.examples/README.md +28 -0
- modmex_ai-0.1.0/.examples/openai/flow.py +299 -0
- modmex_ai-0.1.0/.github/workflows/ci.yml +48 -0
- modmex_ai-0.1.0/.github/workflows/release.yml +89 -0
- modmex_ai-0.1.0/.gitignore +226 -0
- modmex_ai-0.1.0/PKG-INFO +305 -0
- modmex_ai-0.1.0/README.md +284 -0
- modmex_ai-0.1.0/modmex_ai/__init__.py +155 -0
- modmex_ai-0.1.0/modmex_ai/agents/__init__.py +16 -0
- modmex_ai-0.1.0/modmex_ai/agents/agent.py +249 -0
- modmex_ai-0.1.0/modmex_ai/agents/context.py +16 -0
- modmex_ai-0.1.0/modmex_ai/agents/execution.py +172 -0
- modmex_ai-0.1.0/modmex_ai/agents/handoff.py +117 -0
- modmex_ai-0.1.0/modmex_ai/agents/result.py +23 -0
- modmex_ai-0.1.0/modmex_ai/agents/stream.py +28 -0
- modmex_ai-0.1.0/modmex_ai/approvals.py +35 -0
- modmex_ai-0.1.0/modmex_ai/errors.py +107 -0
- modmex_ai-0.1.0/modmex_ai/evals.py +84 -0
- modmex_ai-0.1.0/modmex_ai/flows/__init__.py +15 -0
- modmex_ai-0.1.0/modmex_ai/flows/continuation.py +12 -0
- modmex_ai-0.1.0/modmex_ai/flows/flow.py +608 -0
- modmex_ai-0.1.0/modmex_ai/flows/result.py +32 -0
- modmex_ai-0.1.0/modmex_ai/flows/state.py +86 -0
- modmex_ai-0.1.0/modmex_ai/flows/stream.py +22 -0
- modmex_ai-0.1.0/modmex_ai/guardrails/__init__.py +27 -0
- modmex_ai-0.1.0/modmex_ai/guardrails/guardrail.py +89 -0
- modmex_ai-0.1.0/modmex_ai/guardrails/result.py +11 -0
- modmex_ai-0.1.0/modmex_ai/http/__init__.py +6 -0
- modmex_ai-0.1.0/modmex_ai/http/async_client.py +178 -0
- modmex_ai-0.1.0/modmex_ai/http/client.py +234 -0
- modmex_ai-0.1.0/modmex_ai/http/retries.py +14 -0
- modmex_ai-0.1.0/modmex_ai/http/sse.py +29 -0
- modmex_ai-0.1.0/modmex_ai/messages/__init__.py +4 -0
- modmex_ai-0.1.0/modmex_ai/messages/message.py +17 -0
- modmex_ai-0.1.0/modmex_ai/models/__init__.py +26 -0
- modmex_ai-0.1.0/modmex_ai/models/base.py +29 -0
- modmex_ai-0.1.0/modmex_ai/models/fake.py +39 -0
- modmex_ai-0.1.0/modmex_ai/models/fallback.py +31 -0
- modmex_ai-0.1.0/modmex_ai/models/profile.py +18 -0
- modmex_ai-0.1.0/modmex_ai/models/provider_state.py +17 -0
- modmex_ai-0.1.0/modmex_ai/models/request.py +22 -0
- modmex_ai-0.1.0/modmex_ai/models/response.py +33 -0
- modmex_ai-0.1.0/modmex_ai/models/settings.py +11 -0
- modmex_ai-0.1.0/modmex_ai/models/stream.py +28 -0
- modmex_ai-0.1.0/modmex_ai/models/usage.py +41 -0
- modmex_ai-0.1.0/modmex_ai/observability.py +24 -0
- modmex_ai-0.1.0/modmex_ai/providers/__init__.py +1 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/__init__.py +26 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/audio.py +161 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/chat.py +86 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/mapper.py +478 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/profile.py +22 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/realtime.py +409 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/responses.py +99 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai/transcription.py +190 -0
- modmex_ai-0.1.0/modmex_ai/providers/openai_compatible/__init__.py +4 -0
- modmex_ai-0.1.0/modmex_ai/realtime/__init__.py +5 -0
- modmex_ai-0.1.0/modmex_ai/realtime/event.py +13 -0
- modmex_ai-0.1.0/modmex_ai/realtime/session.py +81 -0
- modmex_ai-0.1.0/modmex_ai/realtime/transport.py +16 -0
- modmex_ai-0.1.0/modmex_ai/schemas/__init__.py +13 -0
- modmex_ai-0.1.0/modmex_ai/schemas/json_schema.py +110 -0
- modmex_ai-0.1.0/modmex_ai/schemas/modmex.py +38 -0
- modmex_ai-0.1.0/modmex_ai/sessions/__init__.py +7 -0
- modmex_ai-0.1.0/modmex_ai/sessions/durable.py +38 -0
- modmex_ai-0.1.0/modmex_ai/sessions/item.py +97 -0
- modmex_ai-0.1.0/modmex_ai/sessions/memory.py +38 -0
- modmex_ai-0.1.0/modmex_ai/sessions/session.py +28 -0
- modmex_ai-0.1.0/modmex_ai/sessions/snapshot.py +38 -0
- modmex_ai-0.1.0/modmex_ai/tools/__init__.py +4 -0
- modmex_ai-0.1.0/modmex_ai/tools/executor.py +44 -0
- modmex_ai-0.1.0/modmex_ai/tools/tool.py +215 -0
- modmex_ai-0.1.0/modmex_ai/tracing/__init__.py +5 -0
- modmex_ai-0.1.0/modmex_ai/tracing/step.py +27 -0
- modmex_ai-0.1.0/modmex_ai/tracing/trace.py +26 -0
- modmex_ai-0.1.0/modmex_ai/voice/__init__.py +49 -0
- modmex_ai-0.1.0/modmex_ai/voice/chained.py +385 -0
- modmex_ai-0.1.0/modmex_ai/voice/continuation.py +19 -0
- modmex_ai-0.1.0/modmex_ai/voice/event.py +37 -0
- modmex_ai-0.1.0/modmex_ai/voice/providers.py +129 -0
- modmex_ai-0.1.0/modmex_ai/voice/session.py +21 -0
- modmex_ai-0.1.0/modmex_ai/voice/turn.py +41 -0
- modmex_ai-0.1.0/poetry.lock +761 -0
- modmex_ai-0.1.0/pyproject.toml +44 -0
- modmex_ai-0.1.0/tests/__init__.py +1 -0
- modmex_ai-0.1.0/tests/agents/__init__.py +1 -0
- modmex_ai-0.1.0/tests/agents/test_agent.py +463 -0
- modmex_ai-0.1.0/tests/agents/test_handoff_edges.py +39 -0
- modmex_ai-0.1.0/tests/agents/test_stream.py +155 -0
- modmex_ai-0.1.0/tests/examples/__init__.py +1 -0
- modmex_ai-0.1.0/tests/examples/test_openai_flow_example.py +214 -0
- modmex_ai-0.1.0/tests/examples/utils.py +15 -0
- modmex_ai-0.1.0/tests/fixtures/openai/flow-call-02-response-empty-payload.json +34 -0
- modmex_ai-0.1.0/tests/flows/__init__.py +1 -0
- modmex_ai-0.1.0/tests/flows/test_flow.py +257 -0
- modmex_ai-0.1.0/tests/flows/test_flow_async.py +59 -0
- modmex_ai-0.1.0/tests/flows/test_flow_errors.py +38 -0
- modmex_ai-0.1.0/tests/guardrails/__init__.py +1 -0
- modmex_ai-0.1.0/tests/guardrails/test_guardrail.py +133 -0
- modmex_ai-0.1.0/tests/http/__init__.py +1 -0
- modmex_ai-0.1.0/tests/http/test_client.py +414 -0
- modmex_ai-0.1.0/tests/http/test_sse.py +21 -0
- modmex_ai-0.1.0/tests/models/__init__.py +1 -0
- modmex_ai-0.1.0/tests/models/test_fake_and_fallback.py +64 -0
- modmex_ai-0.1.0/tests/models/test_usage.py +40 -0
- modmex_ai-0.1.0/tests/providers/__init__.py +1 -0
- modmex_ai-0.1.0/tests/providers/openai/__init__.py +1 -0
- modmex_ai-0.1.0/tests/providers/openai/test_audio.py +132 -0
- modmex_ai-0.1.0/tests/providers/openai/test_mapper.py +402 -0
- modmex_ai-0.1.0/tests/providers/openai/test_models.py +172 -0
- modmex_ai-0.1.0/tests/providers/openai/test_transcription.py +185 -0
- modmex_ai-0.1.0/tests/realtime/__init__.py +0 -0
- modmex_ai-0.1.0/tests/realtime/test_openai_realtime.py +495 -0
- modmex_ai-0.1.0/tests/schemas/__init__.py +1 -0
- modmex_ai-0.1.0/tests/schemas/test_json_schema.py +58 -0
- modmex_ai-0.1.0/tests/schemas/test_modmex.py +51 -0
- modmex_ai-0.1.0/tests/sessions/__init__.py +1 -0
- modmex_ai-0.1.0/tests/sessions/test_memory.py +112 -0
- modmex_ai-0.1.0/tests/test_foundations.py +160 -0
- modmex_ai-0.1.0/tests/tools/__init__.py +1 -0
- modmex_ai-0.1.0/tests/tools/test_tool.py +186 -0
- modmex_ai-0.1.0/tests/voice/__init__.py +0 -0
- modmex_ai-0.1.0/tests/voice/test_chained.py +502 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Live examples
|
|
2
|
+
|
|
3
|
+
These scripts call real provider APIs and may incur cost.
|
|
4
|
+
|
|
5
|
+
They are not part of the test suite and are never run by `pytest`.
|
|
6
|
+
|
|
7
|
+
The OpenAI flow example runs a three-turn pizzeria conversation with the same
|
|
8
|
+
session, so it demonstrates continuation state and can make multiple model
|
|
9
|
+
calls. It includes typed handoffs, a menu tool, typed order output, and an
|
|
10
|
+
output guardrail.
|
|
11
|
+
|
|
12
|
+
Run them manually with an explicit live flag:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
MODMEX_AI_RUN_LIVE=1 OPENAI_API_KEY=... poetry run python .examples/openai/flow.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or use a local env file without adding runtime dependencies:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cp .examples/.env.example .examples/.env
|
|
22
|
+
set -a
|
|
23
|
+
source .examples/.env
|
|
24
|
+
set +a
|
|
25
|
+
poetry run python .examples/openai/flow.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`.examples/.env` is ignored by git.
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Run a complete modmex-ai Flow against the real OpenAI Responses API.
|
|
2
|
+
|
|
3
|
+
This example is intentionally outside `tests/` because it calls a real provider
|
|
4
|
+
and can incur cost.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
|
|
8
|
+
MODMEX_AI_RUN_LIVE=1 OPENAI_API_KEY=... poetry run python .examples/openai/flow.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import field
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
from modmex import BaseModel
|
|
19
|
+
|
|
20
|
+
from modmex_ai import (
|
|
21
|
+
Agent,
|
|
22
|
+
Flow,
|
|
23
|
+
GuardrailResult,
|
|
24
|
+
Handoff,
|
|
25
|
+
InMemorySession,
|
|
26
|
+
prompt_with_handoff_instructions,
|
|
27
|
+
)
|
|
28
|
+
from modmex_ai.errors import ProviderError
|
|
29
|
+
from modmex_ai.providers.openai import OpenAIResponsesModel
|
|
30
|
+
from modmex_ai.schemas import serialize
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TriageHandoffInput(BaseModel):
|
|
34
|
+
intent: Literal["order", "menu", "support", "out_of_scope"]
|
|
35
|
+
reason: str
|
|
36
|
+
confidence: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _record_triage_handoff(context, value: TriageHandoffInput) -> None:
|
|
40
|
+
context.state["triage_handoff"] = serialize(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PizzaOrderPayload(BaseModel):
|
|
44
|
+
size: Literal["small", "medium", "large"] | None = None
|
|
45
|
+
crust: Literal["thin", "regular", "deep_dish"] | None = None
|
|
46
|
+
toppings: list[str] = field(default_factory=list)
|
|
47
|
+
quantity: int | None = None
|
|
48
|
+
customer_name: str | None = None
|
|
49
|
+
delivery_method: Literal["pickup", "delivery"] | None = None
|
|
50
|
+
notes: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PizzaOrderEvent(BaseModel):
|
|
54
|
+
type: Literal[
|
|
55
|
+
"pizza.order.requested",
|
|
56
|
+
"pizza.order.clarification.requested",
|
|
57
|
+
"pizza.order.updated",
|
|
58
|
+
]
|
|
59
|
+
payload: PizzaOrderPayload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OrderOutput(BaseModel):
|
|
63
|
+
event: PizzaOrderEvent
|
|
64
|
+
missing_fields: list[str] = field(default_factory=list)
|
|
65
|
+
reply: str
|
|
66
|
+
confidence: float
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PizzaOrderOutputGuardrail:
|
|
70
|
+
name = "pizza_order_output_consistency"
|
|
71
|
+
|
|
72
|
+
def check(self, output: OrderOutput, context=None) -> GuardrailResult:
|
|
73
|
+
payload = output.event.payload
|
|
74
|
+
required_fields = ("size", "quantity", "crust", "toppings", "delivery_method")
|
|
75
|
+
missing_fields = set(output.missing_fields)
|
|
76
|
+
unknown_fields = {
|
|
77
|
+
field
|
|
78
|
+
for field in required_fields
|
|
79
|
+
if getattr(payload, field) is None
|
|
80
|
+
or field == "toppings" and getattr(payload, field) == []
|
|
81
|
+
}
|
|
82
|
+
if missing_fields != unknown_fields:
|
|
83
|
+
return GuardrailResult(
|
|
84
|
+
passed=False,
|
|
85
|
+
reason=(
|
|
86
|
+
"missing_fields must exactly match required payload fields "
|
|
87
|
+
"whose values are unknown."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
if output.event.type == "pizza.order.clarification.requested" and not missing_fields:
|
|
91
|
+
return GuardrailResult(
|
|
92
|
+
passed=False,
|
|
93
|
+
reason="A clarification event requires at least one missing field.",
|
|
94
|
+
)
|
|
95
|
+
if output.event.type in {"pizza.order.requested", "pizza.order.updated"} and missing_fields:
|
|
96
|
+
return GuardrailResult(
|
|
97
|
+
passed=False,
|
|
98
|
+
reason="A requested or updated order cannot contain missing fields.",
|
|
99
|
+
)
|
|
100
|
+
return GuardrailResult(passed=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def emit_business_events(output, _results) -> list[dict]:
|
|
104
|
+
event = getattr(output, "event", None)
|
|
105
|
+
return [event.model_dump()] if event is not None else []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def require_live_flag() -> None:
|
|
109
|
+
if os.getenv("MODMEX_AI_RUN_LIVE") != "1":
|
|
110
|
+
raise SystemExit(
|
|
111
|
+
"Set MODMEX_AI_RUN_LIVE=1 to run this live API example."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_flow() -> Flow:
|
|
116
|
+
model = OpenAIResponsesModel(
|
|
117
|
+
os.getenv("OPENAI_MODEL", "gpt-4.1-mini"),
|
|
118
|
+
api_key=os.environ["OPENAI_API_KEY"],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
triage = Agent(
|
|
122
|
+
name="triage",
|
|
123
|
+
model=model,
|
|
124
|
+
instructions=(
|
|
125
|
+
"You are a pizzeria triage agent. Your job is to understand what the "
|
|
126
|
+
"customer needs and route the conversation to the best specialist. "
|
|
127
|
+
"Use human_review only when the request is ambiguous, unsafe, or needs "
|
|
128
|
+
"a person. Treat unrelated questions such as math, programming, politics, "
|
|
129
|
+
"medical advice, or general knowledge as out_of_scope."
|
|
130
|
+
),
|
|
131
|
+
handoffs=[
|
|
132
|
+
Handoff(
|
|
133
|
+
"order",
|
|
134
|
+
description=(
|
|
135
|
+
"Use this handoff when the customer wants to place, continue, "
|
|
136
|
+
"or modify a pizza order."
|
|
137
|
+
),
|
|
138
|
+
input_type=TriageHandoffInput,
|
|
139
|
+
on_handoff=_record_triage_handoff,
|
|
140
|
+
),
|
|
141
|
+
Handoff(
|
|
142
|
+
"menu",
|
|
143
|
+
description=(
|
|
144
|
+
"Use this handoff when the customer asks about menu items, "
|
|
145
|
+
"available toppings, sizes, prices, or recommendations."
|
|
146
|
+
),
|
|
147
|
+
input_type=TriageHandoffInput,
|
|
148
|
+
on_handoff=_record_triage_handoff,
|
|
149
|
+
),
|
|
150
|
+
Handoff(
|
|
151
|
+
"support",
|
|
152
|
+
description=(
|
|
153
|
+
"Use this handoff for pizzeria support requests and for "
|
|
154
|
+
"out-of-scope questions that should be politely redirected."
|
|
155
|
+
),
|
|
156
|
+
input_type=TriageHandoffInput,
|
|
157
|
+
on_handoff=_record_triage_handoff,
|
|
158
|
+
),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
order = Agent(
|
|
163
|
+
name="order",
|
|
164
|
+
model=model,
|
|
165
|
+
instructions=prompt_with_handoff_instructions(
|
|
166
|
+
"You are a specialized pizza order agent. Help the customer complete "
|
|
167
|
+
"a pizza order while preserving the order state across the conversation. "
|
|
168
|
+
"Extract only details explicitly stated by the customer or present in a "
|
|
169
|
+
"previous structured pizza-order event. Data integrity rules: never infer "
|
|
170
|
+
"or default any field. In particular, never infer regular crust, pickup, "
|
|
171
|
+
"delivery, a customer name, toppings, or quantity. Use null for an "
|
|
172
|
+
"unknown scalar and [] for unknown toppings. Treat size, quantity, crust, "
|
|
173
|
+
"toppings, and delivery_method as required to request an order; list every "
|
|
174
|
+
"unknown required field in missing_fields. Output consistency is absolute: "
|
|
175
|
+
"a field listed in missing_fields must be null in payload, except toppings "
|
|
176
|
+
"which must be []; a non-null payload value means that field is known and "
|
|
177
|
+
"must not appear in missing_fields. Before responding, verify this rule. "
|
|
178
|
+
"For example, for 'I want two large pizzas', payload.size is 'large' and "
|
|
179
|
+
"payload.quantity is 2, but payload.crust and payload.delivery_method are "
|
|
180
|
+
"null, payload.toppings is [], and missing_fields is exactly ['crust', "
|
|
181
|
+
"'toppings', 'delivery_method']. Return "
|
|
182
|
+
"pizza.order.clarification.requested until those fields are known. Return "
|
|
183
|
+
"pizza.order.requested only when the customer has provided all required "
|
|
184
|
+
"details for the first time. It means the order details are ready for a "
|
|
185
|
+
"downstream service; it does not mean the order was placed or confirmed. "
|
|
186
|
+
"Return pizza.order.updated for a later material change to an existing "
|
|
187
|
+
"order. Always provide a concise customer-facing reply that matches the "
|
|
188
|
+
"event semantics."
|
|
189
|
+
),
|
|
190
|
+
output_type=OrderOutput,
|
|
191
|
+
output_strict=False,
|
|
192
|
+
output_guardrails=[PizzaOrderOutputGuardrail()],
|
|
193
|
+
max_output_guardrail_retries=1,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
menu = Agent(
|
|
197
|
+
name="menu",
|
|
198
|
+
model=model,
|
|
199
|
+
instructions=prompt_with_handoff_instructions(
|
|
200
|
+
"You are a specialized pizzeria menu agent. Answer questions about "
|
|
201
|
+
"menu items, toppings, sizes, prices, and simple recommendations."
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@menu.tool
|
|
206
|
+
def get_menu() -> dict:
|
|
207
|
+
"""Get the current menu."""
|
|
208
|
+
return {
|
|
209
|
+
"sizes": ["small", "medium", "large"],
|
|
210
|
+
"crusts": ["thin", "regular", "deep_dish"],
|
|
211
|
+
"toppings": ["pepperoni", "mushrooms", "onions", "olives"],
|
|
212
|
+
"prices": {
|
|
213
|
+
"small": 8.99,
|
|
214
|
+
"medium": 12.99,
|
|
215
|
+
"large": 15.99,
|
|
216
|
+
"toppings": 1.5,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
support = Agent(
|
|
221
|
+
name="support",
|
|
222
|
+
model=model,
|
|
223
|
+
instructions=prompt_with_handoff_instructions(
|
|
224
|
+
"You are a specialized pizzeria support agent. Handle general support "
|
|
225
|
+
"and boundary messages for the pizzeria."
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return Flow(
|
|
230
|
+
name="analyze-message",
|
|
231
|
+
entrypoint=triage,
|
|
232
|
+
agents=[order, menu, support],
|
|
233
|
+
emit=emit_business_events,
|
|
234
|
+
max_handoffs=2,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def run_demo_conversation(flow: Flow) -> list[dict]:
|
|
239
|
+
session = InMemorySession(session_id="conversation-001")
|
|
240
|
+
turns = [
|
|
241
|
+
"Hi, I want to order two large pizzas.",
|
|
242
|
+
(
|
|
243
|
+
"Make them thin crust with pepperoni and mushrooms, "
|
|
244
|
+
"for pickup under Alex."
|
|
245
|
+
),
|
|
246
|
+
"Please add extra olives to both pizzas.",
|
|
247
|
+
]
|
|
248
|
+
results = []
|
|
249
|
+
continuation = None
|
|
250
|
+
for turn_index, user_message in enumerate(turns, start=1):
|
|
251
|
+
result = flow.run(
|
|
252
|
+
user_message,
|
|
253
|
+
session=session,
|
|
254
|
+
starting_agent=continuation.agent_name if continuation else None,
|
|
255
|
+
provider_state=continuation.provider_state if continuation else None,
|
|
256
|
+
)
|
|
257
|
+
continuation = result.continuation
|
|
258
|
+
results.append({
|
|
259
|
+
"turn": turn_index,
|
|
260
|
+
"input": user_message,
|
|
261
|
+
"starting_agent": result.agent_results[0].agent,
|
|
262
|
+
"last_agent": result.last_agent_name,
|
|
263
|
+
"reply": result.output.reply
|
|
264
|
+
if isinstance(result.output, OrderOutput)
|
|
265
|
+
else result.output,
|
|
266
|
+
"events": result.events,
|
|
267
|
+
})
|
|
268
|
+
return results
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main() -> None:
|
|
272
|
+
require_live_flag()
|
|
273
|
+
flow = build_flow()
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
results = run_demo_conversation(flow)
|
|
277
|
+
except ProviderError as exc:
|
|
278
|
+
payload = {
|
|
279
|
+
"error": str(exc),
|
|
280
|
+
"status_code": exc.status_code,
|
|
281
|
+
"request_id": exc.request_id,
|
|
282
|
+
"response_body": exc.response_body,
|
|
283
|
+
}
|
|
284
|
+
print(json.dumps(payload, indent=2, default=str), flush=True)
|
|
285
|
+
raise SystemExit(1) from exc
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
payload = {
|
|
288
|
+
"error": str(exc),
|
|
289
|
+
"error_type": exc.__class__.__name__,
|
|
290
|
+
}
|
|
291
|
+
print(json.dumps(payload, indent=2, default=str), flush=True)
|
|
292
|
+
raise SystemExit(1) from exc
|
|
293
|
+
|
|
294
|
+
payload = {"turns": results}
|
|
295
|
+
print(json.dumps(payload, indent=2, default=str), flush=True)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: CI Tests
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install Poetry
|
|
28
|
+
run: pip install poetry
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: poetry install
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: poetry run pytest -q
|
|
35
|
+
|
|
36
|
+
- name: Run coverage
|
|
37
|
+
if: matrix.python-version == '3.12'
|
|
38
|
+
run: |
|
|
39
|
+
poetry run coverage run -m pytest
|
|
40
|
+
poetry run coverage report -m
|
|
41
|
+
poetry run coverage xml -o coverage.xml
|
|
42
|
+
|
|
43
|
+
- name: Upload coverage reports to Codecov
|
|
44
|
+
if: matrix.python-version == '3.12' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
45
|
+
uses: codecov/codecov-action@v5
|
|
46
|
+
with:
|
|
47
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
48
|
+
files: ./coverage.xml
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
tags:
|
|
8
|
+
- "v*"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
name: Test Python
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Setup Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
- name: Install Poetry
|
|
29
|
+
run: pip install poetry
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: poetry install
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: poetry run pytest -q
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
name: Build distribution
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
needs: test
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout
|
|
44
|
+
uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- name: Setup Python
|
|
47
|
+
uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
|
|
51
|
+
- name: Install Poetry
|
|
52
|
+
run: pip install poetry
|
|
53
|
+
|
|
54
|
+
- name: Install dependencies
|
|
55
|
+
run: poetry install
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies
|
|
58
|
+
run: poetry build
|
|
59
|
+
|
|
60
|
+
- name: Upload sdist
|
|
61
|
+
uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: distributions
|
|
64
|
+
path: dist/*
|
|
65
|
+
if-no-files-found: error
|
|
66
|
+
|
|
67
|
+
publish:
|
|
68
|
+
name: Publish to PyPI
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
needs: build
|
|
71
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
72
|
+
environment:
|
|
73
|
+
name: pypi
|
|
74
|
+
url: https://pypi.org/p/modmex-ai
|
|
75
|
+
permissions:
|
|
76
|
+
contents: read
|
|
77
|
+
id-token: write
|
|
78
|
+
|
|
79
|
+
steps:
|
|
80
|
+
- name: Download distributions
|
|
81
|
+
uses: actions/download-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
name: distributions
|
|
84
|
+
path: dist
|
|
85
|
+
|
|
86
|
+
- name: Publish distributions
|
|
87
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
88
|
+
with:
|
|
89
|
+
packages-dir: dist
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
.benchmark/
|
|
220
|
+
.vscode/
|
|
221
|
+
|
|
222
|
+
# Local live examples
|
|
223
|
+
.examples/.env
|
|
224
|
+
.examples/output/
|
|
225
|
+
.doc/
|
|
226
|
+
|