agentspec-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.
Files changed (59) hide show
  1. agentspec/__init__.py +13 -0
  2. agentspec/__main__.py +6 -0
  3. agentspec/adapters/__init__.py +45 -0
  4. agentspec/adapters/autogen.py +116 -0
  5. agentspec/adapters/crewai.py +102 -0
  6. agentspec/adapters/generic.py +165 -0
  7. agentspec/adapters/langchain.py +131 -0
  8. agentspec/adapters/openai_agents.py +111 -0
  9. agentspec/badge/__init__.py +1 -0
  10. agentspec/badge/generator.py +58 -0
  11. agentspec/badge/scorer.py +258 -0
  12. agentspec/badge/templates/.gitkeep +0 -0
  13. agentspec/ci/__init__.py +11 -0
  14. agentspec/ci/github.py +52 -0
  15. agentspec/ci/gitlab.py +33 -0
  16. agentspec/ci/pre_commit.py +58 -0
  17. agentspec/cli/__init__.py +1 -0
  18. agentspec/cli/app.py +645 -0
  19. agentspec/cli/diff_cmd.py +144 -0
  20. agentspec/cli/init_cmd.py +103 -0
  21. agentspec/cli/report_cmd.py +409 -0
  22. agentspec/cli/serve.py +58 -0
  23. agentspec/core/__init__.py +1 -0
  24. agentspec/core/linter.py +846 -0
  25. agentspec/core/loader.py +171 -0
  26. agentspec/core/models.py +560 -0
  27. agentspec/core/resolver.py +97 -0
  28. agentspec/core/validator.py +244 -0
  29. agentspec/generator/__init__.py +13 -0
  30. agentspec/generator/analyzer.py +168 -0
  31. agentspec/generator/llm_generator.py +132 -0
  32. agentspec/generator/scanner.py +268 -0
  33. agentspec/generator/templates.py +103 -0
  34. agentspec/runtime/__init__.py +46 -0
  35. agentspec/runtime/circuit_breaker.py +145 -0
  36. agentspec/runtime/decorators.py +90 -0
  37. agentspec/runtime/frameworks/__init__.py +16 -0
  38. agentspec/runtime/frameworks/generic_wrapper.py +151 -0
  39. agentspec/runtime/frameworks/langchain_middleware.py +243 -0
  40. agentspec/runtime/guards.py +236 -0
  41. agentspec/runtime/middleware.py +427 -0
  42. agentspec/runtime/tracker.py +168 -0
  43. agentspec/testing/__init__.py +1 -0
  44. agentspec/testing/assertions.py +274 -0
  45. agentspec/testing/generator.py +221 -0
  46. agentspec/testing/reporters/__init__.py +1 -0
  47. agentspec/testing/reporters/console.py +43 -0
  48. agentspec/testing/reporters/html.py +120 -0
  49. agentspec/testing/reporters/json.py +25 -0
  50. agentspec/testing/reporters/junit.py +54 -0
  51. agentspec/testing/runner.py +300 -0
  52. agentspec/utils/__init__.py +1 -0
  53. agentspec/utils/hash.py +47 -0
  54. agentspec/utils/yaml_utils.py +87 -0
  55. agentspec_ai-0.1.0.dist-info/METADATA +824 -0
  56. agentspec_ai-0.1.0.dist-info/RECORD +59 -0
  57. agentspec_ai-0.1.0.dist-info/WHEEL +4 -0
  58. agentspec_ai-0.1.0.dist-info/entry_points.txt +4 -0
  59. agentspec_ai-0.1.0.dist-info/licenses/LICENSE +94 -0
agentspec/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """AgentSpec — The OpenAPI for Agentic AI.
2
+
3
+ A universal specification standard for AI agent behavioral contracts.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __spec_version__ = "0.1.0"
8
+
9
+ from agentspec.core.models import AgentSpecDocument
10
+ from agentspec.core.loader import SpecLoader
11
+ from agentspec.core.validator import SpecValidator
12
+
13
+ __all__ = ["AgentSpecDocument", "SpecLoader", "SpecValidator", "__version__", "__spec_version__"]
agentspec/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running agentspec as a module: python -m agentspec."""
2
+
3
+ from agentspec.cli.app import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,45 @@
1
+ """Framework adapters for connecting AgentSpec to agent frameworks.
2
+
3
+ Provides abstract base class and registry for adapter plugins.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any
10
+
11
+
12
+ class AgentAdapter(ABC):
13
+ """Abstract base class for all framework adapters."""
14
+
15
+ name: str = "base"
16
+
17
+ @abstractmethod
18
+ def connect(self, source: str, **kwargs: Any) -> None:
19
+ """Connect to an agent instance."""
20
+
21
+ @abstractmethod
22
+ def extract_spec(self) -> dict[str, Any]:
23
+ """Extract a partial AgentSpec from the connected agent."""
24
+
25
+ @abstractmethod
26
+ def disconnect(self) -> None:
27
+ """Disconnect from the agent."""
28
+
29
+
30
+ class AdapterRegistry:
31
+ """Registry for discovering and loading adapters."""
32
+
33
+ _adapters: dict[str, type[AgentAdapter]] = {}
34
+
35
+ @classmethod
36
+ def register(cls, name: str, adapter_cls: type[AgentAdapter]) -> None:
37
+ cls._adapters[name] = adapter_cls
38
+
39
+ @classmethod
40
+ def get(cls, name: str) -> type[AgentAdapter] | None:
41
+ return cls._adapters.get(name)
42
+
43
+ @classmethod
44
+ def list_adapters(cls) -> list[str]:
45
+ return list(cls._adapters.keys())
@@ -0,0 +1,116 @@
1
+ """AutoGen framework adapter.
2
+
3
+ Connects to AutoGen ConversableAgent or AssistantAgent and extracts
4
+ a partial AgentSpec.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from agentspec.adapters import AgentAdapter, AdapterRegistry
12
+
13
+
14
+ class AutoGenAdapter(AgentAdapter):
15
+ """Adapter for Microsoft AutoGen agents.
16
+
17
+ Supports ConversableAgent, AssistantAgent, and UserProxyAgent.
18
+ Extracts system messages, registered functions, and model config.
19
+ """
20
+
21
+ name = "autogen"
22
+
23
+ def __init__(self) -> None:
24
+ self._agent: Any = None
25
+ self._system_message: str | None = None
26
+ self._functions: list[dict[str, Any]] = []
27
+ self._model: str | None = None
28
+
29
+ def connect(self, source: str, **kwargs: Any) -> None:
30
+ """Connect to an AutoGen agent by importing its module.
31
+
32
+ Args:
33
+ source: Python module path to the agent.
34
+ The module must expose an ``agent`` attribute.
35
+ """
36
+ import importlib
37
+
38
+ try:
39
+ module = importlib.import_module(source)
40
+ self._agent = getattr(module, "agent", None) or getattr(module, "assistant", None)
41
+ except ImportError as exc:
42
+ raise ConnectionError(f"Cannot import AutoGen agent from '{source}': {exc}") from exc
43
+
44
+ if self._agent is not None:
45
+ self._system_message = getattr(self._agent, "system_message", None)
46
+ self._extract_model()
47
+ self._extract_functions()
48
+
49
+ def disconnect(self) -> None:
50
+ self._agent = None
51
+
52
+ def extract_spec(self) -> dict[str, Any]:
53
+ """Extract a partial AgentSpec from the AutoGen agent."""
54
+ capabilities = [
55
+ {
56
+ "id": _to_kebab(fn.get("name", f"fn-{i}")),
57
+ "name": fn.get("name", f"Function {i}"),
58
+ "description": fn.get("description", ""),
59
+ "category": "tool-use",
60
+ }
61
+ for i, fn in enumerate(self._functions)
62
+ ]
63
+
64
+ # Add a default reasoning capability based on system message
65
+ if self._system_message and not capabilities:
66
+ capabilities.append({
67
+ "id": "agent-reasoning",
68
+ "name": "Agent Reasoning",
69
+ "description": self._system_message[:200],
70
+ "category": "reasoning",
71
+ })
72
+
73
+ spec: dict[str, Any] = {
74
+ "agentspec": "0.1.0",
75
+ "info": {"name": "autogen-agent", "version": "0.1.0"},
76
+ "agent": {
77
+ "type": "semi-autonomous",
78
+ "runtime": {
79
+ "framework": "autogen",
80
+ "model": self._model or "gpt-4o",
81
+ "model_provider": "openai",
82
+ },
83
+ },
84
+ "capabilities": capabilities or [{"id": "default-task", "name": "Default Task", "category": "reasoning"}],
85
+ "boundaries": [],
86
+ "guardrails": {"behavioral_guardrails": {}},
87
+ "failure_modes": [],
88
+ }
89
+ return spec
90
+
91
+ def _extract_model(self) -> None:
92
+ if self._agent is None:
93
+ return
94
+ llm_config = getattr(self._agent, "llm_config", None) or {}
95
+ if isinstance(llm_config, dict):
96
+ configs = llm_config.get("config_list", [])
97
+ if configs and isinstance(configs[0], dict):
98
+ self._model = configs[0].get("model")
99
+
100
+ def _extract_functions(self) -> None:
101
+ if self._agent is None:
102
+ return
103
+ # AutoGen stores function_map as a dict
104
+ fn_map = getattr(self._agent, "function_map", {}) or {}
105
+ for name, fn in fn_map.items():
106
+ self._functions.append({
107
+ "name": name,
108
+ "description": getattr(fn, "__doc__", "") or "",
109
+ })
110
+
111
+
112
+ def _to_kebab(name: str) -> str:
113
+ return name.replace("_", "-").replace(" ", "-").lower()
114
+
115
+
116
+ AdapterRegistry.register("autogen", AutoGenAdapter)
@@ -0,0 +1,102 @@
1
+ """CrewAI framework adapter.
2
+
3
+ Maps CrewAI Agent/Crew/Task concepts to AgentSpec sections.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from agentspec.adapters import AgentAdapter, AdapterRegistry
11
+
12
+
13
+ class CrewAIAdapter(AgentAdapter):
14
+ """Adapter for CrewAI Crew objects.
15
+
16
+ Maps Crew → multi-agent-orchestrator, Agent → agent,
17
+ Task → capability, Tool → tool.
18
+ """
19
+
20
+ name = "crewai"
21
+
22
+ def __init__(self) -> None:
23
+ self._crew: Any = None
24
+ self._agents: list[Any] = []
25
+ self._tasks: list[Any] = []
26
+ self._tools: list[Any] = []
27
+
28
+ def connect(self, source: str, **kwargs: Any) -> None:
29
+ """Connect to a CrewAI Crew by importing its module.
30
+
31
+ Args:
32
+ source: Python module path to the crew (e.g., "my_crew.crew").
33
+ The module must expose a ``crew`` attribute.
34
+ """
35
+ import importlib
36
+
37
+ try:
38
+ module = importlib.import_module(source)
39
+ self._crew = getattr(module, "crew", None)
40
+ except ImportError as exc:
41
+ raise ConnectionError(f"Cannot import CrewAI crew from '{source}': {exc}") from exc
42
+
43
+ if self._crew is not None:
44
+ self._agents = list(getattr(self._crew, "agents", []) or [])
45
+ self._tasks = list(getattr(self._crew, "tasks", []) or [])
46
+ # Collect tools from all agents
47
+ for agent in self._agents:
48
+ self._tools.extend(getattr(agent, "tools", []) or [])
49
+
50
+ def disconnect(self) -> None:
51
+ """Release references."""
52
+ self._crew = None
53
+ self._agents = []
54
+ self._tasks = []
55
+ self._tools = []
56
+
57
+ def extract_spec(self) -> dict[str, Any]:
58
+ """Extract a partial spec from the CrewAI crew."""
59
+ capabilities = [
60
+ {
61
+ "id": _to_kebab(getattr(task, "description", f"task-{i}")[:40]),
62
+ "name": getattr(task, "name", f"Task {i}") or f"Task {i}",
63
+ "description": getattr(task, "description", ""),
64
+ "category": "reasoning",
65
+ }
66
+ for i, task in enumerate(self._tasks)
67
+ ]
68
+
69
+ tools_spec = [
70
+ {
71
+ "id": _to_kebab(getattr(t, "name", f"tool-{i}")),
72
+ "name": getattr(t, "name", f"Tool {i}"),
73
+ "protocol": "custom",
74
+ "permissions": ["read"],
75
+ }
76
+ for i, t in enumerate(self._tools)
77
+ ]
78
+
79
+ agent_type = "multi-agent-orchestrator" if len(self._agents) > 1 else "autonomous"
80
+
81
+ spec: dict[str, Any] = {
82
+ "agentspec": "0.1.0",
83
+ "info": {"name": "crewai-crew", "version": "0.1.0"},
84
+ "agent": {
85
+ "type": agent_type,
86
+ "runtime": {"framework": "crewai"},
87
+ },
88
+ "capabilities": capabilities,
89
+ "boundaries": [],
90
+ "guardrails": {"behavioral_guardrails": {}},
91
+ "failure_modes": [],
92
+ }
93
+ if tools_spec:
94
+ spec["tools"] = tools_spec
95
+ return spec
96
+
97
+
98
+ def _to_kebab(name: str) -> str:
99
+ return name.replace("_", "-").replace(" ", "-").lower().strip("-")[:40]
100
+
101
+
102
+ AdapterRegistry.register("crewai", CrewAIAdapter)
@@ -0,0 +1,165 @@
1
+ """Generic HTTP/callable adapter.
2
+
3
+ Provides a flexible adapter for any agent exposed as:
4
+ - A Python callable (function or class with .invoke())
5
+ - An HTTP endpoint
6
+ - An MCP-compatible server (stdio or HTTP/SSE)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any, Callable
14
+
15
+ from agentspec.adapters import AgentAdapter, AdapterRegistry
16
+
17
+
18
+ class GenericAdapter(AgentAdapter):
19
+ """Generic adapter connecting to any callable, HTTP endpoint, or MCP server.
20
+
21
+ Args:
22
+ auth_header: Optional ``Authorization`` header value (e.g., "Bearer <token>").
23
+ response_path: JSONPath-style dot-notation to extract the response text
24
+ (e.g., "choices.0.message.content"). Defaults to the raw body.
25
+ """
26
+
27
+ name = "generic"
28
+
29
+ def __init__(
30
+ self,
31
+ auth_header: str | None = None,
32
+ response_path: str | None = None,
33
+ ) -> None:
34
+ self._source: str | None = None
35
+ self._callable: Callable[[str], str] | None = None
36
+ self._endpoint: str | None = None
37
+ self._auth_header = auth_header
38
+ self._response_path = response_path
39
+ self._discovered_tools: list[dict[str, Any]] = []
40
+
41
+ def connect(self, source: str, **kwargs: Any) -> None:
42
+ """Connect to a generic source.
43
+
44
+ Args:
45
+ source: Either a Python module path (e.g., ``mymodule.agent``),
46
+ an HTTP URL (e.g., ``http://localhost:8080/invoke``), or
47
+ an MCP server path (e.g., ``mcp://localhost:8081``).
48
+ """
49
+ self._source = source
50
+
51
+ if source.startswith(("http://", "https://")):
52
+ self._endpoint = source
53
+ self._probe_http()
54
+ elif source.startswith("mcp://"):
55
+ self._probe_mcp(source)
56
+ else:
57
+ # Assume Python module path
58
+ import importlib
59
+
60
+ try:
61
+ module = importlib.import_module(source)
62
+ agent = (
63
+ getattr(module, "agent", None)
64
+ or getattr(module, "app", None)
65
+ or getattr(module, "invoke", None)
66
+ )
67
+ if callable(agent):
68
+ self._callable = agent
69
+ except ImportError as exc:
70
+ raise ConnectionError(f"Cannot connect to '{source}': {exc}") from exc
71
+
72
+ def disconnect(self) -> None:
73
+ self._callable = None
74
+ self._endpoint = None
75
+ self._source = None
76
+
77
+ def extract_spec(self) -> dict[str, Any]:
78
+ """Generate a partial spec from discovered information."""
79
+ capabilities = [
80
+ {
81
+ "id": t["id"],
82
+ "name": t["name"],
83
+ "description": t.get("description", ""),
84
+ "category": "tool-use",
85
+ }
86
+ for t in self._discovered_tools
87
+ ] or [{"id": "default-capability", "name": "Default Capability", "category": "reasoning"}]
88
+
89
+ agent_type = "tool-agent" if self._discovered_tools else "reactive"
90
+
91
+ spec: dict[str, Any] = {
92
+ "agentspec": "0.1.0",
93
+ "info": {"name": _source_to_name(self._source or "generic-agent"), "version": "0.1.0"},
94
+ "agent": {
95
+ "type": agent_type,
96
+ "runtime": {"framework": "custom"},
97
+ },
98
+ "capabilities": capabilities,
99
+ "boundaries": [],
100
+ "guardrails": {"behavioral_guardrails": {}},
101
+ "failure_modes": [],
102
+ }
103
+
104
+ if self._endpoint:
105
+ spec["agent"]["protocol"] = "http"
106
+
107
+ return spec
108
+
109
+ def _probe_http(self) -> None:
110
+ """Attempt to probe the HTTP endpoint for OpenAPI or tool definitions."""
111
+ if not self._endpoint:
112
+ return
113
+ try:
114
+ import httpx
115
+
116
+ headers: dict[str, str] = {}
117
+ if self._auth_header:
118
+ headers["Authorization"] = self._auth_header
119
+
120
+ # Try /.well-known/agent.json (A2A-style)
121
+ for probe_path in ["/tools", "/capabilities", "/.well-known/agent.json"]:
122
+ try:
123
+ url = self._endpoint.rstrip("/") + probe_path
124
+ resp = httpx.get(url, headers=headers, timeout=5.0)
125
+ if resp.status_code == 200:
126
+ data = resp.json()
127
+ self._parse_tool_list(data)
128
+ break
129
+ except Exception:
130
+ continue
131
+ except ImportError:
132
+ pass # httpx not available
133
+
134
+ def _probe_mcp(self, source: str) -> None:
135
+ """Attempt to probe an MCP server for tool definitions."""
136
+ # MCP probing would require an MCP client library; stub with no-op
137
+ self._discovered_tools = []
138
+
139
+ def _parse_tool_list(self, data: Any) -> None:
140
+ """Parse a JSON tool list into discovered_tools."""
141
+ if isinstance(data, list):
142
+ for item in data:
143
+ if isinstance(item, dict):
144
+ self._discovered_tools.append({
145
+ "id": _to_kebab(item.get("name", "tool")),
146
+ "name": item.get("name", "Tool"),
147
+ "description": item.get("description", ""),
148
+ })
149
+ elif isinstance(data, dict):
150
+ tools = data.get("tools") or data.get("capabilities") or []
151
+ self._parse_tool_list(tools)
152
+
153
+
154
+ def _to_kebab(name: str) -> str:
155
+ return name.replace("_", "-").replace(" ", "-").lower()
156
+
157
+
158
+ def _source_to_name(source: str) -> str:
159
+ """Derive a short agent name from the source string."""
160
+ if "/" in source or "\\" in source:
161
+ return Path(source).stem.replace("_", "-")
162
+ return source.split(".")[-1].replace("_", "-")
163
+
164
+
165
+ AdapterRegistry.register("generic", GenericAdapter)
@@ -0,0 +1,131 @@
1
+ """LangChain framework adapter.
2
+
3
+ Connects to a LangChain agent (AgentExecutor or LangGraph) and extracts
4
+ a partial AgentSpec, including detected tools and system prompts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from agentspec.adapters import AgentAdapter, AdapterRegistry
12
+
13
+
14
+ class LangChainAdapter(AgentAdapter):
15
+ """Adapter for LangChain agents (AgentExecutor, LangGraph chains).
16
+
17
+ Supports direct Python import connection. Extracts tools from the
18
+ agent's tool list and system prompt from the model's messages.
19
+ """
20
+
21
+ name = "langchain"
22
+
23
+ def __init__(self) -> None:
24
+ self._agent: Any = None
25
+ self._tools: list[dict[str, Any]] = []
26
+ self._system_prompt: str | None = None
27
+ self._model: str | None = None
28
+
29
+ def connect(self, source: str, **kwargs: Any) -> None:
30
+ """Connect to a LangChain agent by importing its module.
31
+
32
+ Args:
33
+ source: Python module path to the agent (e.g., "my_agent.agent").
34
+ The module must expose an ``agent`` attribute.
35
+ """
36
+ import importlib
37
+
38
+ try:
39
+ module = importlib.import_module(source)
40
+ self._agent = getattr(module, "agent", None) or getattr(module, "executor", None)
41
+ except ImportError as exc:
42
+ raise ConnectionError(f"Cannot import LangChain agent from '{source}': {exc}") from exc
43
+
44
+ if self._agent is not None:
45
+ self._extract_tools()
46
+ self._extract_model()
47
+
48
+ def disconnect(self) -> None:
49
+ """Release the reference to the connected agent."""
50
+ self._agent = None
51
+
52
+ def extract_spec(self) -> dict[str, Any]:
53
+ """Extract a partial AgentSpec from the connected agent.
54
+
55
+ Returns:
56
+ Partial spec dict with detected tools, capabilities, and agent type.
57
+ """
58
+ capabilities = [
59
+ {
60
+ "id": t["id"],
61
+ "name": t["name"],
62
+ "description": t.get("description", ""),
63
+ "category": "tool-use",
64
+ }
65
+ for t in self._tools
66
+ ]
67
+ tools = [
68
+ {
69
+ "id": t["id"],
70
+ "name": t["name"],
71
+ "protocol": "custom",
72
+ "permissions": ["read"],
73
+ }
74
+ for t in self._tools
75
+ ]
76
+
77
+ spec: dict[str, Any] = {
78
+ "agentspec": "0.1.0",
79
+ "info": {"name": "langchain-agent", "version": "0.1.0"},
80
+ "agent": {
81
+ "type": "autonomous",
82
+ "runtime": {
83
+ "framework": "langchain",
84
+ "model": self._model or "unknown",
85
+ },
86
+ },
87
+ "capabilities": capabilities,
88
+ "boundaries": [],
89
+ "guardrails": {"behavioral_guardrails": {}},
90
+ "failure_modes": [],
91
+ }
92
+ if tools:
93
+ spec["tools"] = tools
94
+ return spec
95
+
96
+ def _extract_tools(self) -> None:
97
+ """Pull tool definitions from the agent."""
98
+ if self._agent is None:
99
+ return
100
+
101
+ raw_tools = getattr(self._agent, "tools", None)
102
+ if not raw_tools:
103
+ return
104
+
105
+ for tool in raw_tools:
106
+ self._tools.append({
107
+ "id": _to_kebab(getattr(tool, "name", "unknown-tool")),
108
+ "name": getattr(tool, "name", "Unknown"),
109
+ "description": getattr(tool, "description", ""),
110
+ })
111
+
112
+ def _extract_model(self) -> None:
113
+ """Try to detect the underlying model name."""
114
+ if self._agent is None:
115
+ return
116
+ llm = getattr(self._agent, "llm", None) or getattr(self._agent, "llm_chain", None)
117
+ if llm:
118
+ model_name = (
119
+ getattr(llm, "model_name", None)
120
+ or getattr(llm, "model", None)
121
+ )
122
+ if model_name:
123
+ self._model = str(model_name)
124
+
125
+
126
+ def _to_kebab(name: str) -> str:
127
+ """Convert a snake_case or space name to kebab-case."""
128
+ return name.replace("_", "-").replace(" ", "-").lower()
129
+
130
+
131
+ AdapterRegistry.register("langchain", LangChainAdapter)
@@ -0,0 +1,111 @@
1
+ """OpenAI Agents (Responses API) adapter.
2
+
3
+ Maps OpenAI function definitions and instructions to AgentSpec sections.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from agentspec.adapters import AgentAdapter, AdapterRegistry
11
+
12
+
13
+ class OpenAIAgentsAdapter(AgentAdapter):
14
+ """Adapter for OpenAI Agents SDK / Responses API.
15
+
16
+ Extracts function tools and system instructions to generate a partial spec.
17
+ """
18
+
19
+ name = "openai"
20
+
21
+ def __init__(self) -> None:
22
+ self._agent: Any = None
23
+ self._functions: list[dict[str, Any]] = []
24
+ self._instructions: str | None = None
25
+ self._model: str | None = None
26
+
27
+ def connect(self, source: str, **kwargs: Any) -> None:
28
+ """Connect to an OpenAI agent by importing its module.
29
+
30
+ Args:
31
+ source: Python module path to the agent.
32
+ The module must expose an ``agent`` attribute.
33
+ """
34
+ import importlib
35
+
36
+ try:
37
+ module = importlib.import_module(source)
38
+ self._agent = getattr(module, "agent", None) or getattr(module, "assistant", None)
39
+ except ImportError as exc:
40
+ raise ConnectionError(f"Cannot import OpenAI agent from '{source}': {exc}") from exc
41
+
42
+ if self._agent is not None:
43
+ self._extract_functions()
44
+ self._instructions = getattr(self._agent, "instructions", None)
45
+ self._model = getattr(self._agent, "model", None)
46
+
47
+ def disconnect(self) -> None:
48
+ self._agent = None
49
+
50
+ def extract_spec(self) -> dict[str, Any]:
51
+ """Extract a partial AgentSpec from the OpenAI agent."""
52
+ capabilities = [
53
+ {
54
+ "id": _to_kebab(fn.get("name", f"fn-{i}")),
55
+ "name": fn.get("name", f"Function {i}"),
56
+ "description": fn.get("description", ""),
57
+ "category": "tool-use",
58
+ }
59
+ for i, fn in enumerate(self._functions)
60
+ ]
61
+ tools = [
62
+ {
63
+ "id": _to_kebab(fn.get("name", f"fn-{i}")),
64
+ "name": fn.get("name", f"Function {i}"),
65
+ "protocol": "custom",
66
+ "permissions": ["execute"],
67
+ }
68
+ for i, fn in enumerate(self._functions)
69
+ ]
70
+
71
+ spec: dict[str, Any] = {
72
+ "agentspec": "0.1.0",
73
+ "info": {"name": "openai-agent", "version": "0.1.0"},
74
+ "agent": {
75
+ "type": "tool-agent",
76
+ "runtime": {
77
+ "framework": "openai",
78
+ "model": self._model or "gpt-4o",
79
+ "model_provider": "openai",
80
+ },
81
+ },
82
+ "capabilities": capabilities,
83
+ "boundaries": [],
84
+ "guardrails": {"behavioral_guardrails": {}},
85
+ "failure_modes": [],
86
+ }
87
+ if tools:
88
+ spec["tools"] = tools
89
+ return spec
90
+
91
+ def _extract_functions(self) -> None:
92
+ if self._agent is None:
93
+ return
94
+ # OpenAI SDK stores tools as list of dicts with "type"/"function" or direct callables
95
+ raw = getattr(self._agent, "tools", None) or getattr(self._agent, "functions", None) or []
96
+ for item in raw:
97
+ if isinstance(item, dict):
98
+ fn = item.get("function") or item
99
+ self._functions.append(fn)
100
+ elif callable(item):
101
+ self._functions.append({
102
+ "name": getattr(item, "__name__", "unknown"),
103
+ "description": getattr(item, "__doc__", "") or "",
104
+ })
105
+
106
+
107
+ def _to_kebab(name: str) -> str:
108
+ return name.replace("_", "-").replace(" ", "-").lower()
109
+
110
+
111
+ AdapterRegistry.register("openai", OpenAIAgentsAdapter)