mutiny-openai-agents 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.
- mutiny_openai_agents-0.1.0/.gitignore +31 -0
- mutiny_openai_agents-0.1.0/PKG-INFO +12 -0
- mutiny_openai_agents-0.1.0/pyproject.toml +31 -0
- mutiny_openai_agents-0.1.0/src/mutiny_openai_agents/__init__.py +12 -0
- mutiny_openai_agents-0.1.0/src/mutiny_openai_agents/adapter.py +138 -0
- mutiny_openai_agents-0.1.0/src/mutiny_openai_agents/extract.py +144 -0
- mutiny_openai_agents-0.1.0/src/mutiny_openai_agents/loader.py +99 -0
- mutiny_openai_agents-0.1.0/src/mutiny_openai_agents/sample.py +34 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.eggs/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
.DS_Store
|
|
17
|
+
*.sqlite
|
|
18
|
+
*.db
|
|
19
|
+
data/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
node_modules/
|
|
23
|
+
.next/
|
|
24
|
+
.worktrees/
|
|
25
|
+
.mutiny/
|
|
26
|
+
!examples/openai_support_agent/.mutiny/
|
|
27
|
+
!examples/openai_support_agent/.mutiny/**
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mutiny-openai-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mutiny Adapter #1 — OpenAI Agents SDK TargetAdapter
|
|
5
|
+
Project-URL: Homepage, https://github.com/CodewithJha/mutiny
|
|
6
|
+
Project-URL: Repository, https://github.com/CodewithJha/mutiny
|
|
7
|
+
Project-URL: Issues, https://github.com/CodewithJha/mutiny/issues
|
|
8
|
+
Author-email: Priyanshu Jha <155089480+CodewithJha@users.noreply.github.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: mutiny-core>=0.1.0
|
|
12
|
+
Requires-Dist: openai-agents>=0.19
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mutiny-openai-agents"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Mutiny Adapter #1 — OpenAI Agents SDK TargetAdapter"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Priyanshu Jha", email = "155089480+CodewithJha@users.noreply.github.com" },
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mutiny-core>=0.1.0",
|
|
12
|
+
"openai-agents>=0.19",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/CodewithJha/mutiny"
|
|
17
|
+
Repository = "https://github.com/CodewithJha/mutiny"
|
|
18
|
+
Issues = "https://github.com/CodewithJha/mutiny/issues"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/mutiny_openai_agents"]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.sdist]
|
|
28
|
+
include = [
|
|
29
|
+
"/src",
|
|
30
|
+
"/pyproject.toml",
|
|
31
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""OpenAI Agents SDK adapter — Mutiny Adapter #1 (ADR-018)."""
|
|
2
|
+
|
|
3
|
+
from mutiny_openai_agents.adapter import OpenAIAgentsAdapter
|
|
4
|
+
from mutiny_openai_agents.loader import load_agent_from_ref, load_callable
|
|
5
|
+
from mutiny_openai_agents.sample import make_openai_support_adapter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"OpenAIAgentsAdapter",
|
|
9
|
+
"load_agent_from_ref",
|
|
10
|
+
"load_callable",
|
|
11
|
+
"make_openai_support_adapter",
|
|
12
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""OpenAIAgentsAdapter — TargetAdapter for OpenAI Agents SDK projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agents import Agent, Runner
|
|
10
|
+
from agents.memory.sqlite_session import SQLiteSession
|
|
11
|
+
|
|
12
|
+
from mutiny_core.adapter.port import TargetAdapter, ToolsNotObservableError
|
|
13
|
+
from mutiny_core.trace.models import AdapterTurnResult
|
|
14
|
+
|
|
15
|
+
from mutiny_openai_agents.extract import (
|
|
16
|
+
extract_assistant_message,
|
|
17
|
+
extract_tool_calls,
|
|
18
|
+
extract_tool_results,
|
|
19
|
+
summarize_items,
|
|
20
|
+
tools_observable,
|
|
21
|
+
)
|
|
22
|
+
from mutiny_openai_agents.loader import load_agent_from_ref
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenAIAgentsAdapter(TargetAdapter):
|
|
26
|
+
"""Drive an OpenAI Agents SDK ``Agent`` and observe tool calls.
|
|
27
|
+
|
|
28
|
+
Framework-specific imports stay inside this package (ADR-018). Core only
|
|
29
|
+
sees ``TargetAdapter`` / ``AdapterTurnResult``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
agent: Agent[Any] | None = None,
|
|
35
|
+
*,
|
|
36
|
+
agent_ref: str | None = None,
|
|
37
|
+
agent_factory: Callable[[], Agent[Any]] | None = None,
|
|
38
|
+
context: dict[str, Any] | None = None,
|
|
39
|
+
context_provider: Callable[[], dict[str, Any]] | None = None,
|
|
40
|
+
session_factory: Callable[[str], Any] | None = None,
|
|
41
|
+
on_reset: Callable[[], None] | None = None,
|
|
42
|
+
max_turns: int = 10,
|
|
43
|
+
) -> None:
|
|
44
|
+
if agent is None and agent_ref is None and agent_factory is None:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"OpenAIAgentsAdapter requires agent=, agent_ref=, or agent_factory="
|
|
47
|
+
)
|
|
48
|
+
self._agent = agent
|
|
49
|
+
self._agent_ref = agent_ref
|
|
50
|
+
self._agent_factory = agent_factory
|
|
51
|
+
self._context = context or {}
|
|
52
|
+
self._context_provider = context_provider
|
|
53
|
+
self._session_factory = session_factory or (
|
|
54
|
+
lambda sid: SQLiteSession(sid, db_path=":memory:")
|
|
55
|
+
)
|
|
56
|
+
self._on_reset = on_reset
|
|
57
|
+
self._max_turns = max_turns
|
|
58
|
+
self._sessions: dict[str, Any] = {}
|
|
59
|
+
self._resolved_agent: Agent[Any] | None = None
|
|
60
|
+
|
|
61
|
+
def reset(self, session_id: str) -> None:
|
|
62
|
+
"""Start a fresh conversation session (in-memory SQLite by default)."""
|
|
63
|
+
if self._on_reset is not None:
|
|
64
|
+
self._on_reset()
|
|
65
|
+
previous = self._sessions.pop(session_id, None)
|
|
66
|
+
if previous is not None and hasattr(previous, "close"):
|
|
67
|
+
try:
|
|
68
|
+
previous.close()
|
|
69
|
+
except Exception: # noqa: BLE001
|
|
70
|
+
pass
|
|
71
|
+
self._sessions[session_id] = self._session_factory(session_id)
|
|
72
|
+
|
|
73
|
+
def step(self, session_id: str, user_message: str) -> AdapterTurnResult:
|
|
74
|
+
"""Run one user turn via ``Runner.run_sync``; map tools → Mutiny traces."""
|
|
75
|
+
if session_id not in self._sessions:
|
|
76
|
+
self.reset(session_id)
|
|
77
|
+
|
|
78
|
+
agent = self._get_agent()
|
|
79
|
+
session = self._sessions[session_id]
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = Runner.run_sync(
|
|
83
|
+
agent,
|
|
84
|
+
user_message,
|
|
85
|
+
session=session,
|
|
86
|
+
max_turns=self._max_turns,
|
|
87
|
+
)
|
|
88
|
+
except ToolsNotObservableError:
|
|
89
|
+
raise
|
|
90
|
+
except Exception as exc: # noqa: BLE001
|
|
91
|
+
# Surface SDK failures without inventing tool evidence
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"OpenAI Agents SDK run failed for session={session_id}: {exc}"
|
|
94
|
+
) from exc
|
|
95
|
+
|
|
96
|
+
if not tools_observable(result):
|
|
97
|
+
raise ToolsNotObservableError(
|
|
98
|
+
"OpenAI Agents SDK result does not expose new_items; "
|
|
99
|
+
"cannot observe tool calls for policy evidence"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
tool_calls = extract_tool_calls(result)
|
|
103
|
+
tool_results = extract_tool_results(result)
|
|
104
|
+
assistant = extract_assistant_message(result)
|
|
105
|
+
|
|
106
|
+
return AdapterTurnResult(
|
|
107
|
+
assistant_message=assistant,
|
|
108
|
+
tool_calls=tool_calls,
|
|
109
|
+
tool_results=tool_results,
|
|
110
|
+
raw={
|
|
111
|
+
"session_id": session_id,
|
|
112
|
+
"adapter": "openai_agents",
|
|
113
|
+
"item_summary": summarize_items(result),
|
|
114
|
+
"last_agent": getattr(
|
|
115
|
+
getattr(result, "last_agent", None), "name", None
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def context(self, session_id: str | None = None) -> dict[str, Any]:
|
|
121
|
+
"""Deterministic facts for policy evaluation."""
|
|
122
|
+
_ = session_id
|
|
123
|
+
if self._context_provider is not None:
|
|
124
|
+
return copy.deepcopy(self._context_provider())
|
|
125
|
+
return copy.deepcopy(self._context)
|
|
126
|
+
|
|
127
|
+
def _get_agent(self) -> Agent[Any]:
|
|
128
|
+
if self._resolved_agent is not None:
|
|
129
|
+
return self._resolved_agent
|
|
130
|
+
if self._agent is not None:
|
|
131
|
+
self._resolved_agent = self._agent
|
|
132
|
+
return self._resolved_agent
|
|
133
|
+
if self._agent_factory is not None:
|
|
134
|
+
self._resolved_agent = self._agent_factory()
|
|
135
|
+
return self._resolved_agent
|
|
136
|
+
assert self._agent_ref is not None
|
|
137
|
+
self._resolved_agent = load_agent_from_ref(self._agent_ref)
|
|
138
|
+
return self._resolved_agent
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Map OpenAI Agents SDK RunResult items → Mutiny ToolCall / messages.
|
|
2
|
+
|
|
3
|
+
All OpenAI Agents SDK imports stay here (and in adapter.py) — never in Core.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agents import ItemHelpers
|
|
12
|
+
from agents.items import MessageOutputItem, ToolCallItem, ToolCallOutputItem
|
|
13
|
+
from agents.result import RunResultBase
|
|
14
|
+
|
|
15
|
+
from mutiny_core.trace.models import ToolCall
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_assistant_message(result: RunResultBase) -> str | None:
|
|
19
|
+
"""Best-effort assistant text from a run result."""
|
|
20
|
+
if result.final_output is not None:
|
|
21
|
+
out = result.final_output
|
|
22
|
+
if isinstance(out, str):
|
|
23
|
+
return out
|
|
24
|
+
return str(out)
|
|
25
|
+
try:
|
|
26
|
+
text = ItemHelpers.text_message_outputs(list(result.new_items))
|
|
27
|
+
except Exception: # noqa: BLE001
|
|
28
|
+
text = ""
|
|
29
|
+
return text or None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_tool_calls(result: RunResultBase) -> list[ToolCall]:
|
|
33
|
+
"""Collect function/tool calls observed during the run."""
|
|
34
|
+
calls: list[ToolCall] = []
|
|
35
|
+
for item in result.new_items:
|
|
36
|
+
if not _is_tool_call_item(item):
|
|
37
|
+
continue
|
|
38
|
+
name = getattr(item, "tool_name", None) or _raw_name(
|
|
39
|
+
getattr(item, "raw_item", None)
|
|
40
|
+
)
|
|
41
|
+
if not name:
|
|
42
|
+
continue
|
|
43
|
+
call_id = getattr(item, "call_id", None) or f"oa-tc-{len(calls) + 1}"
|
|
44
|
+
arguments = _parse_arguments(getattr(item, "raw_item", None))
|
|
45
|
+
calls.append(ToolCall(id=str(call_id), name=name, arguments=arguments))
|
|
46
|
+
return calls
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_tool_results(result: RunResultBase) -> list[dict[str, Any]]:
|
|
50
|
+
"""Collect tool outputs paired with call ids when available."""
|
|
51
|
+
results: list[dict[str, Any]] = []
|
|
52
|
+
for item in result.new_items:
|
|
53
|
+
if not _is_tool_output_item(item):
|
|
54
|
+
continue
|
|
55
|
+
payload: dict[str, Any] = {
|
|
56
|
+
"call_id": getattr(item, "call_id", None),
|
|
57
|
+
"output": _normalize_output(getattr(item, "output", None)),
|
|
58
|
+
}
|
|
59
|
+
results.append(payload)
|
|
60
|
+
return results
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tools_observable(result: RunResultBase) -> bool:
|
|
64
|
+
"""True if this run path can surface tool call evidence.
|
|
65
|
+
|
|
66
|
+
Empty tool_calls is fine (agent may not call tools). We fail only when
|
|
67
|
+
the result object lacks the observation surfaces Mutiny requires.
|
|
68
|
+
"""
|
|
69
|
+
return hasattr(result, "new_items")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def summarize_items(result: RunResultBase) -> list[str]:
|
|
73
|
+
"""Debug helper: item type names from the run."""
|
|
74
|
+
names: list[str] = []
|
|
75
|
+
for item in result.new_items:
|
|
76
|
+
if _is_tool_call_item(item):
|
|
77
|
+
names.append(f"tool_call:{getattr(item, 'tool_name', None)}")
|
|
78
|
+
elif _is_tool_output_item(item):
|
|
79
|
+
names.append(f"tool_output:{getattr(item, 'call_id', None)}")
|
|
80
|
+
elif isinstance(item, MessageOutputItem) or getattr(
|
|
81
|
+
item, "type", None
|
|
82
|
+
) == "message_output_item":
|
|
83
|
+
names.append("message")
|
|
84
|
+
else:
|
|
85
|
+
names.append(type(item).__name__)
|
|
86
|
+
return names
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_tool_call_item(item: Any) -> bool:
|
|
90
|
+
if isinstance(item, ToolCallItem):
|
|
91
|
+
return True
|
|
92
|
+
return getattr(item, "type", None) == "tool_call_item"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_tool_output_item(item: Any) -> bool:
|
|
96
|
+
if isinstance(item, ToolCallOutputItem):
|
|
97
|
+
return True
|
|
98
|
+
return getattr(item, "type", None) == "tool_call_output_item"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _raw_name(raw_item: Any) -> str | None:
|
|
102
|
+
if raw_item is None:
|
|
103
|
+
return None
|
|
104
|
+
if isinstance(raw_item, dict):
|
|
105
|
+
name = raw_item.get("name")
|
|
106
|
+
return str(name) if name else None
|
|
107
|
+
name = getattr(raw_item, "name", None)
|
|
108
|
+
return str(name) if name else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_arguments(raw_item: Any) -> dict[str, Any]:
|
|
112
|
+
if raw_item is None:
|
|
113
|
+
return {}
|
|
114
|
+
raw_args: Any
|
|
115
|
+
if isinstance(raw_item, dict):
|
|
116
|
+
raw_args = raw_item.get("arguments", {})
|
|
117
|
+
else:
|
|
118
|
+
raw_args = getattr(raw_item, "arguments", {})
|
|
119
|
+
if raw_args is None:
|
|
120
|
+
return {}
|
|
121
|
+
if isinstance(raw_args, dict):
|
|
122
|
+
return dict(raw_args)
|
|
123
|
+
if isinstance(raw_args, str):
|
|
124
|
+
if not raw_args.strip():
|
|
125
|
+
return {}
|
|
126
|
+
try:
|
|
127
|
+
parsed = json.loads(raw_args)
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
return {"_raw": raw_args}
|
|
130
|
+
if isinstance(parsed, dict):
|
|
131
|
+
return parsed
|
|
132
|
+
return {"value": parsed}
|
|
133
|
+
return {"value": raw_args}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _normalize_output(output: Any) -> Any:
|
|
137
|
+
if isinstance(output, (dict, list, str, int, float, bool)) or output is None:
|
|
138
|
+
return output
|
|
139
|
+
if hasattr(output, "model_dump"):
|
|
140
|
+
try:
|
|
141
|
+
return output.model_dump()
|
|
142
|
+
except Exception: # noqa: BLE001
|
|
143
|
+
return str(output)
|
|
144
|
+
return str(output)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Load customer agent modules by ``module:attr`` reference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_project_on_path(project_root: str | Path) -> None:
|
|
13
|
+
"""Prepend a customer project root to ``sys.path`` for local imports."""
|
|
14
|
+
root = str(Path(project_root).resolve())
|
|
15
|
+
if root not in sys.path:
|
|
16
|
+
sys.path.insert(0, root)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_callable(ref: str) -> Any:
|
|
20
|
+
"""Import ``module:attr`` and return the attribute (object or callable)."""
|
|
21
|
+
if ":" not in ref:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"invalid ref {ref!r}; expected 'module:attribute' "
|
|
24
|
+
"(e.g. 'agent:support_agent' or 'agent:build_agent')"
|
|
25
|
+
)
|
|
26
|
+
module_name, attr_name = ref.split(":", 1)
|
|
27
|
+
if not module_name or not attr_name:
|
|
28
|
+
raise ValueError(f"invalid ref {ref!r}; module and attribute required")
|
|
29
|
+
module = importlib.import_module(module_name)
|
|
30
|
+
try:
|
|
31
|
+
return getattr(module, attr_name)
|
|
32
|
+
except AttributeError as exc:
|
|
33
|
+
raise AttributeError(
|
|
34
|
+
f"module {module_name!r} has no attribute {attr_name!r}"
|
|
35
|
+
) from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_agent_from_ref(ref: str) -> Any:
|
|
39
|
+
"""Resolve an OpenAI Agents SDK Agent from ``module:attr``.
|
|
40
|
+
|
|
41
|
+
If the attribute is callable (and not already an Agent-like instance with
|
|
42
|
+
``name`` + ``tools``), it is called with no arguments.
|
|
43
|
+
"""
|
|
44
|
+
obj = load_callable(ref)
|
|
45
|
+
if callable(obj) and not _looks_like_agent(obj):
|
|
46
|
+
obj = obj()
|
|
47
|
+
if not _looks_like_agent(obj):
|
|
48
|
+
raise TypeError(
|
|
49
|
+
f"{ref!r} did not resolve to an OpenAI Agents SDK Agent "
|
|
50
|
+
f"(got {type(obj)!r})"
|
|
51
|
+
)
|
|
52
|
+
return obj
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_adapter_factory(
|
|
56
|
+
project_root: str | Path,
|
|
57
|
+
*,
|
|
58
|
+
module: str = ".mutiny.adapter",
|
|
59
|
+
attr: str = "create_adapter",
|
|
60
|
+
) -> Callable[[], Any]:
|
|
61
|
+
"""Load ``create_adapter`` from the customer's ``.mutiny/adapter.py``.
|
|
62
|
+
|
|
63
|
+
Supports either package-style ``.mutiny.adapter`` (if ``.mutiny`` is a
|
|
64
|
+
package) or a direct file load of ``.mutiny/adapter.py``.
|
|
65
|
+
"""
|
|
66
|
+
root = Path(project_root).resolve()
|
|
67
|
+
ensure_project_on_path(root)
|
|
68
|
+
|
|
69
|
+
adapter_file = root / ".mutiny" / "adapter.py"
|
|
70
|
+
if not adapter_file.exists():
|
|
71
|
+
raise FileNotFoundError(
|
|
72
|
+
f"missing {adapter_file}; run `mutiny init` first"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Load as a unique module from file path so `.mutiny` need not be a package
|
|
76
|
+
spec_name = "mutiny_project_adapter"
|
|
77
|
+
import importlib.util
|
|
78
|
+
|
|
79
|
+
spec = importlib.util.spec_from_file_location(spec_name, adapter_file)
|
|
80
|
+
if spec is None or spec.loader is None:
|
|
81
|
+
raise ImportError(f"cannot load adapter module from {adapter_file}")
|
|
82
|
+
mod = importlib.util.module_from_spec(spec)
|
|
83
|
+
sys.modules[spec_name] = mod
|
|
84
|
+
spec.loader.exec_module(mod)
|
|
85
|
+
|
|
86
|
+
factory = getattr(mod, attr, None)
|
|
87
|
+
if factory is None or not callable(factory):
|
|
88
|
+
raise AttributeError(
|
|
89
|
+
f"{adapter_file} must define callable {attr}() → TargetAdapter"
|
|
90
|
+
)
|
|
91
|
+
return factory # type: ignore[return-value]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _looks_like_agent(obj: Any) -> bool:
|
|
95
|
+
return (
|
|
96
|
+
hasattr(obj, "name")
|
|
97
|
+
and hasattr(obj, "tools")
|
|
98
|
+
and not isinstance(obj, type)
|
|
99
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Hosted / CLI helpers for the sample OpenAI Agents SDK project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mutiny_openai_agents.adapter import OpenAIAgentsAdapter
|
|
8
|
+
from mutiny_openai_agents.loader import load_adapter_factory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def sample_project_root(repo_root: Path | None = None) -> Path:
|
|
12
|
+
"""Locate ``examples/openai_support_agent`` from the Mutiny repo root."""
|
|
13
|
+
if repo_root is None:
|
|
14
|
+
# packages/mutiny_openai_agents/src/mutiny_openai_agents/sample.py
|
|
15
|
+
repo_root = Path(__file__).resolve().parents[4]
|
|
16
|
+
return repo_root / "examples" / "openai_support_agent"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_openai_support_adapter(
|
|
20
|
+
*,
|
|
21
|
+
repo_root: Path | None = None,
|
|
22
|
+
project_root: Path | None = None,
|
|
23
|
+
enforce_refund_policy: bool = False,
|
|
24
|
+
) -> OpenAIAgentsAdapter:
|
|
25
|
+
"""Build adapter from the sample project's ``.mutiny/adapter.py``.
|
|
26
|
+
|
|
27
|
+
Same loader Hosted uses for any customer OpenAI Agents SDK project.
|
|
28
|
+
Always returns ``OpenAIAgentsAdapter`` — never the legacy in-process demo.
|
|
29
|
+
"""
|
|
30
|
+
root = project_root or sample_project_root(repo_root)
|
|
31
|
+
if not root.exists():
|
|
32
|
+
raise FileNotFoundError(f"sample project not found: {root}")
|
|
33
|
+
factory = load_adapter_factory(root)
|
|
34
|
+
return factory(enforce_refund_policy=enforce_refund_policy)
|