api-operator 0.9.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.
- api_operator/__init__.py +8 -0
- api_operator/adapters/__init__.py +1 -0
- api_operator/adapters/base.py +25 -0
- api_operator/adapters/mock.py +145 -0
- api_operator/adapters/openapi_generator.py +238 -0
- api_operator/adapters/registry.py +71 -0
- api_operator/adapters/yaml_adapter.py +175 -0
- api_operator/adapters/yaml_spec.py +154 -0
- api_operator/core/agent.py +160 -0
- api_operator/core/config.py +18 -0
- api_operator/core/executor.py +38 -0
- api_operator/core/guardrails.py +53 -0
- api_operator/core/memory.py +46 -0
- api_operator/core/planner.py +189 -0
- api_operator/core/planner_openai.py +71 -0
- api_operator/factory.py +26 -0
- api_operator/rag/indexer.py +37 -0
- api_operator/server/app.py +114 -0
- api_operator/server/cli.py +201 -0
- api_operator/tools/base.py +69 -0
- api_operator/tools/schema.py +34 -0
- api_operator-0.9.0.dist-info/METADATA +206 -0
- api_operator-0.9.0.dist-info/RECORD +26 -0
- api_operator-0.9.0.dist-info/WHEEL +4 -0
- api_operator-0.9.0.dist-info/entry_points.txt +2 -0
- api_operator-0.9.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from api_operator.core.config import Settings
|
|
7
|
+
from api_operator.core.planner import MockPlanner, PlanStep, Planner
|
|
8
|
+
from api_operator.tools.base import ToolRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenAIPlanner:
|
|
12
|
+
def __init__(self, settings: Settings) -> None:
|
|
13
|
+
self.settings = settings
|
|
14
|
+
|
|
15
|
+
async def plan(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
registry: ToolRegistry,
|
|
19
|
+
history: list[dict[str, str]],
|
|
20
|
+
system_prompt: str,
|
|
21
|
+
) -> PlanStep:
|
|
22
|
+
try:
|
|
23
|
+
from openai import AsyncOpenAI
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
raise RuntimeError(
|
|
26
|
+
"OpenAI planner requires optional dependency. Install with: pip install api-operator[llm]"
|
|
27
|
+
) from exc
|
|
28
|
+
|
|
29
|
+
if not self.settings.openai_api_key:
|
|
30
|
+
raise RuntimeError("api_operator_OPENAI_API_KEY is not set.")
|
|
31
|
+
|
|
32
|
+
client = AsyncOpenAI(api_key=self.settings.openai_api_key)
|
|
33
|
+
tools = [_openai_tool_schema(tool.schema()) for tool in registry.list_tools()]
|
|
34
|
+
messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
|
35
|
+
messages.extend(history[-12:])
|
|
36
|
+
messages.append({"role": "user", "content": message})
|
|
37
|
+
|
|
38
|
+
response = await client.chat.completions.create(
|
|
39
|
+
model=self.settings.openai_model,
|
|
40
|
+
messages=messages,
|
|
41
|
+
tools=tools or None,
|
|
42
|
+
tool_choice="auto" if tools else None,
|
|
43
|
+
)
|
|
44
|
+
choice = response.choices[0].message
|
|
45
|
+
|
|
46
|
+
if choice.tool_calls:
|
|
47
|
+
call = choice.tool_calls[0]
|
|
48
|
+
args = json.loads(call.function.arguments or "{}")
|
|
49
|
+
return PlanStep(type="tool", tool_name=call.function.name, tool_args=args)
|
|
50
|
+
|
|
51
|
+
return PlanStep(type="respond", content=choice.content or "")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _openai_tool_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
return {
|
|
56
|
+
"type": "function",
|
|
57
|
+
"function": {
|
|
58
|
+
"name": schema["name"],
|
|
59
|
+
"description": schema["description"],
|
|
60
|
+
"parameters": schema["parameters"],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_planner(settings: Settings) -> Planner:
|
|
66
|
+
mode = settings.planner
|
|
67
|
+
if mode == "auto":
|
|
68
|
+
mode = "openai" if settings.openai_api_key else "mock"
|
|
69
|
+
if mode == "openai":
|
|
70
|
+
return OpenAIPlanner(settings)
|
|
71
|
+
return MockPlanner()
|
api_operator/factory.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from api_operator.core.agent import Agent
|
|
6
|
+
from api_operator.core.config import Settings
|
|
7
|
+
from api_operator.core.memory import SessionStore
|
|
8
|
+
from api_operator.adapters.registry import load_adapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_agent(
|
|
12
|
+
adapter_name: str = "mock",
|
|
13
|
+
settings: Settings | None = None,
|
|
14
|
+
adapter_class: str | None = None,
|
|
15
|
+
config_path: str | None = None,
|
|
16
|
+
sessions: SessionStore | None = None,
|
|
17
|
+
**adapter_kwargs: Any,
|
|
18
|
+
) -> Agent:
|
|
19
|
+
settings = settings or Settings()
|
|
20
|
+
adapter = load_adapter(
|
|
21
|
+
adapter_name,
|
|
22
|
+
adapter_class=adapter_class,
|
|
23
|
+
config_path=config_path,
|
|
24
|
+
**adapter_kwargs,
|
|
25
|
+
)
|
|
26
|
+
return Agent(adapter=adapter, settings=settings, sessions=sessions)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
class DocsIndex:
|
|
7
|
+
"""Lightweight keyword RAG over markdown/text docs (no embeddings required)."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, paths: list[str | Path]) -> None:
|
|
10
|
+
self.chunks: list[tuple[str, str]] = []
|
|
11
|
+
for path in paths:
|
|
12
|
+
file_path = Path(path)
|
|
13
|
+
if not file_path.exists():
|
|
14
|
+
continue
|
|
15
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
16
|
+
source = str(file_path)
|
|
17
|
+
for idx, paragraph in enumerate(text.split("\n\n")):
|
|
18
|
+
cleaned = paragraph.strip()
|
|
19
|
+
if len(cleaned) < 40:
|
|
20
|
+
continue
|
|
21
|
+
self.chunks.append((source, cleaned))
|
|
22
|
+
|
|
23
|
+
def search(self, query: str, limit: int = 3) -> list[dict[str, Any]]:
|
|
24
|
+
terms = [t for t in query.lower().split() if len(t) > 2]
|
|
25
|
+
if not terms:
|
|
26
|
+
return []
|
|
27
|
+
scored: list[tuple[int, str, str]] = []
|
|
28
|
+
for source, chunk in self.chunks:
|
|
29
|
+
lower = chunk.lower()
|
|
30
|
+
score = sum(lower.count(term) for term in terms)
|
|
31
|
+
if score:
|
|
32
|
+
scored.append((score, source, chunk))
|
|
33
|
+
scored.sort(key=lambda item: item[0], reverse=True)
|
|
34
|
+
return [
|
|
35
|
+
{"source": source, "excerpt": chunk[:500]}
|
|
36
|
+
for _, source, chunk in scored[:limit]
|
|
37
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from api_operator.adapters.registry import available_adapters
|
|
8
|
+
from api_operator.core.config import Settings
|
|
9
|
+
from api_operator.core.memory import SessionStore
|
|
10
|
+
from api_operator.factory import build_agent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChatRequest(BaseModel):
|
|
14
|
+
message: str
|
|
15
|
+
session_id: str | None = None
|
|
16
|
+
adapter: str | None = None
|
|
17
|
+
adapter_class: str | None = None
|
|
18
|
+
config_path: str | None = None
|
|
19
|
+
abilities: list[str] | None = None
|
|
20
|
+
auto_confirm: bool = False
|
|
21
|
+
adapter_config: dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChatResponse(BaseModel):
|
|
25
|
+
session_id: str
|
|
26
|
+
message: str
|
|
27
|
+
status: str
|
|
28
|
+
tool: str | None = None
|
|
29
|
+
tool_result: dict[str, Any] | None = None
|
|
30
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_app(settings: Settings | None = None):
|
|
34
|
+
from fastapi import FastAPI, HTTPException
|
|
35
|
+
|
|
36
|
+
settings = settings or Settings()
|
|
37
|
+
session_store = SessionStore()
|
|
38
|
+
app = FastAPI(
|
|
39
|
+
title="API Operator",
|
|
40
|
+
version="0.1.0",
|
|
41
|
+
description="Standalone AI operator with pluggable adapters",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@app.get("/health")
|
|
45
|
+
async def health() -> dict[str, str]:
|
|
46
|
+
return {"status": "ok", "adapter_default": settings.default_adapter}
|
|
47
|
+
|
|
48
|
+
@app.get("/v1/adapters")
|
|
49
|
+
async def adapters() -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"builtin": available_adapters(),
|
|
52
|
+
"yaml": "Use adapter=yaml with config_path in adapter_config",
|
|
53
|
+
"external": "Use adapter_class=module.path:ClassName (see examples/)",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@app.get("/v1/tools")
|
|
57
|
+
async def tools(
|
|
58
|
+
adapter: str | None = None,
|
|
59
|
+
adapter_class: str | None = None,
|
|
60
|
+
config_path: str | None = None,
|
|
61
|
+
base_url: str | None = None,
|
|
62
|
+
token: str | None = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
name = adapter or settings.default_adapter
|
|
65
|
+
config: dict[str, Any] = {}
|
|
66
|
+
if config_path:
|
|
67
|
+
config["config_path"] = config_path
|
|
68
|
+
if base_url:
|
|
69
|
+
config["base_url"] = base_url
|
|
70
|
+
if token:
|
|
71
|
+
config["token"] = token
|
|
72
|
+
agent = build_agent(
|
|
73
|
+
name,
|
|
74
|
+
settings=settings,
|
|
75
|
+
adapter_class=adapter_class,
|
|
76
|
+
config_path=config_path,
|
|
77
|
+
base_url=base_url,
|
|
78
|
+
token=token,
|
|
79
|
+
)
|
|
80
|
+
return {"adapter": name, "adapter_class": adapter_class, "tools": agent.list_tools()}
|
|
81
|
+
|
|
82
|
+
@app.post("/v1/chat", response_model=ChatResponse)
|
|
83
|
+
async def chat(payload: ChatRequest) -> ChatResponse:
|
|
84
|
+
name = payload.adapter or settings.default_adapter
|
|
85
|
+
extra = dict(payload.adapter_config)
|
|
86
|
+
cfg = payload.config_path or extra.pop("config_path", None)
|
|
87
|
+
try:
|
|
88
|
+
agent = build_agent(
|
|
89
|
+
name,
|
|
90
|
+
settings=settings,
|
|
91
|
+
adapter_class=payload.adapter_class,
|
|
92
|
+
config_path=cfg,
|
|
93
|
+
sessions=session_store,
|
|
94
|
+
**extra,
|
|
95
|
+
)
|
|
96
|
+
except (ValueError, TypeError) as exc:
|
|
97
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
98
|
+
|
|
99
|
+
response = await agent.chat(
|
|
100
|
+
message=payload.message,
|
|
101
|
+
session_id=payload.session_id,
|
|
102
|
+
abilities=payload.abilities,
|
|
103
|
+
auto_confirm=payload.auto_confirm,
|
|
104
|
+
)
|
|
105
|
+
return ChatResponse(
|
|
106
|
+
session_id=response.session_id,
|
|
107
|
+
message=response.message,
|
|
108
|
+
status=response.status,
|
|
109
|
+
tool=response.tool,
|
|
110
|
+
tool_result=response.tool_result,
|
|
111
|
+
metadata=response.metadata,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return app
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from api_operator.adapters.mock import reset_mock_store
|
|
13
|
+
from api_operator.adapters.openapi_generator import generate_adapter_from_openapi, scaffold_adapter
|
|
14
|
+
from api_operator.adapters.yaml_spec import save_adapter_spec
|
|
15
|
+
from api_operator.core.config import Settings
|
|
16
|
+
from api_operator.factory import build_agent
|
|
17
|
+
from api_operator.server.app import create_app
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(no_args_is_help=True, help="API Operator CLI")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_agent_from_cli(
|
|
24
|
+
adapter: str,
|
|
25
|
+
adapter_class: str | None,
|
|
26
|
+
config: str | None,
|
|
27
|
+
planner: str,
|
|
28
|
+
base_url: str | None,
|
|
29
|
+
token: str | None,
|
|
30
|
+
) -> Any:
|
|
31
|
+
settings = Settings(planner=planner, default_adapter=adapter)
|
|
32
|
+
kwargs: dict[str, Any] = {}
|
|
33
|
+
if base_url:
|
|
34
|
+
kwargs["base_url"] = base_url
|
|
35
|
+
if token:
|
|
36
|
+
kwargs["token"] = token
|
|
37
|
+
if adapter == "yaml" and not config:
|
|
38
|
+
console.print("[red]--adapter yaml requires --config path/to/adapter.yaml[/red]")
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
return build_agent(
|
|
41
|
+
adapter,
|
|
42
|
+
settings=settings,
|
|
43
|
+
adapter_class=adapter_class,
|
|
44
|
+
config_path=config,
|
|
45
|
+
**kwargs,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command("chat")
|
|
50
|
+
def chat(
|
|
51
|
+
adapter: str = typer.Option("mock", help="Built-in adapter: mock | yaml"),
|
|
52
|
+
config: str | None = typer.Option(None, help="Path to adapter.yaml (required for yaml adapter)"),
|
|
53
|
+
adapter_class: str | None = typer.Option(None, help="External adapter class module:Class"),
|
|
54
|
+
session_id: str | None = typer.Option(None, help="Reuse session id"),
|
|
55
|
+
planner: str = typer.Option("mock", help="Planner: mock | openai | auto"),
|
|
56
|
+
base_url: str | None = typer.Option(None, help="Override API base URL from YAML"),
|
|
57
|
+
token: str | None = typer.Option(None, help="API bearer token"),
|
|
58
|
+
auto_confirm: bool = typer.Option(False, help="Skip dangerous confirmations"),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Interactive chat in the terminal."""
|
|
61
|
+
agent = _build_agent_from_cli(adapter, adapter_class, config, planner, base_url, token)
|
|
62
|
+
sid = session_id
|
|
63
|
+
label = config or adapter_class or adapter
|
|
64
|
+
console.print(f"[bold green]API Operator[/bold green] adapter={label} planner={planner}")
|
|
65
|
+
console.print("Type 'exit' to quit.\n")
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
try:
|
|
69
|
+
user_input = console.input("[bold cyan]you>[/bold cyan] ")
|
|
70
|
+
except (EOFError, KeyboardInterrupt):
|
|
71
|
+
console.print("\nBye.")
|
|
72
|
+
break
|
|
73
|
+
if user_input.strip().lower() in {"exit", "quit"}:
|
|
74
|
+
break
|
|
75
|
+
response = asyncio.run(
|
|
76
|
+
agent.chat(
|
|
77
|
+
message=user_input,
|
|
78
|
+
session_id=sid,
|
|
79
|
+
abilities=_default_abilities(adapter),
|
|
80
|
+
auto_confirm=auto_confirm,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
sid = response.session_id
|
|
84
|
+
color = {"ok": "green", "confirm": "yellow", "error": "red"}.get(response.status, "white")
|
|
85
|
+
console.print(f"[bold {color}]agent>[/bold {color}] {response.message}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command("serve")
|
|
89
|
+
def serve(
|
|
90
|
+
host: str = typer.Option("127.0.0.1", help="Host"),
|
|
91
|
+
port: int = typer.Option(8100, help="Port"),
|
|
92
|
+
adapter: str = typer.Option("mock", help="Default adapter"),
|
|
93
|
+
planner: str = typer.Option("mock", help="Planner mode"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Run HTTP API server."""
|
|
96
|
+
import uvicorn
|
|
97
|
+
|
|
98
|
+
settings = Settings(host=host, port=port, default_adapter=adapter, planner=planner)
|
|
99
|
+
uvicorn.run(create_app(settings), host=host, port=port)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command("tools")
|
|
103
|
+
def tools(
|
|
104
|
+
adapter: str = typer.Option("mock", help="Adapter name"),
|
|
105
|
+
config: str | None = typer.Option(None, help="adapter.yaml path for yaml adapter"),
|
|
106
|
+
base_url: str | None = typer.Option(None, help="Override base URL"),
|
|
107
|
+
token: str | None = typer.Option(None, help="API token"),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""List registered tools for an adapter."""
|
|
110
|
+
agent = _build_agent_from_cli(adapter, None, config, "mock", base_url, token)
|
|
111
|
+
table = Table(title=f"Tools ({config or adapter})")
|
|
112
|
+
table.add_column("Name")
|
|
113
|
+
table.add_column("Dangerous")
|
|
114
|
+
table.add_column("Ability")
|
|
115
|
+
table.add_column("Description")
|
|
116
|
+
for schema in agent.list_tools():
|
|
117
|
+
table.add_row(
|
|
118
|
+
schema["name"],
|
|
119
|
+
str(schema.get("dangerous", False)),
|
|
120
|
+
str(schema.get("requires_ability") or "-"),
|
|
121
|
+
schema["description"],
|
|
122
|
+
)
|
|
123
|
+
console.print(table)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("scaffold-adapter")
|
|
127
|
+
def scaffold_cmd(
|
|
128
|
+
name: str = typer.Argument(..., help="Adapter/project name"),
|
|
129
|
+
output: str = typer.Option("examples", help="Output directory"),
|
|
130
|
+
base_url: str = typer.Option("http://localhost:8000", help="Default API base URL"),
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Create adapter.yaml + README template (no Python required)."""
|
|
133
|
+
out_dir = Path(output) / f"{name}-adapter"
|
|
134
|
+
path = scaffold_adapter(name, out_dir, base_url=base_url)
|
|
135
|
+
console.print(f"[green]Created[/green] {path}")
|
|
136
|
+
console.print(f"Edit the YAML, then run:\n python -m api_operator.server.cli chat --adapter yaml --config {path}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command("generate-from-openapi")
|
|
140
|
+
def generate_openapi_cmd(
|
|
141
|
+
spec: str = typer.Argument(..., help="OpenAPI YAML/JSON file"),
|
|
142
|
+
output: str = typer.Option("adapter.yaml", help="Output adapter.yaml path"),
|
|
143
|
+
base_url: str = typer.Option(..., help="API base URL"),
|
|
144
|
+
path_prefix: str | None = typer.Option("/api", help="Only include paths starting with this prefix"),
|
|
145
|
+
methods: str = typer.Option("GET,POST", help="Comma-separated HTTP methods to include"),
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Generate adapter.yaml from an OpenAPI spec."""
|
|
148
|
+
include = {m.strip().upper() for m in methods.split(",") if m.strip()}
|
|
149
|
+
adapter_spec = generate_adapter_from_openapi(
|
|
150
|
+
spec,
|
|
151
|
+
base_url=base_url,
|
|
152
|
+
path_prefix=path_prefix,
|
|
153
|
+
include_methods=include,
|
|
154
|
+
)
|
|
155
|
+
save_adapter_spec(adapter_spec, output)
|
|
156
|
+
console.print(f"[green]Generated[/green] {output} with {len(adapter_spec.tools)} tools")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command("demo")
|
|
160
|
+
def demo() -> None:
|
|
161
|
+
"""Run an automated mock demo conversation."""
|
|
162
|
+
reset_mock_store()
|
|
163
|
+
agent = build_agent("mock", settings=Settings(planner="mock"))
|
|
164
|
+
|
|
165
|
+
async def run() -> None:
|
|
166
|
+
steps = [
|
|
167
|
+
"list workspaces",
|
|
168
|
+
"create workspace Acme subdomain acme",
|
|
169
|
+
"yes",
|
|
170
|
+
"invite admin@acme.com to acme admin",
|
|
171
|
+
"yes",
|
|
172
|
+
"list workspaces",
|
|
173
|
+
"provision link Riyadh Jeddah 500",
|
|
174
|
+
"yes",
|
|
175
|
+
"list connections",
|
|
176
|
+
]
|
|
177
|
+
sid = None
|
|
178
|
+
for step in steps:
|
|
179
|
+
console.print(f"[cyan]you>[/cyan] {step}")
|
|
180
|
+
response = await agent.chat(
|
|
181
|
+
message=step,
|
|
182
|
+
session_id=sid,
|
|
183
|
+
abilities=_default_abilities("mock"),
|
|
184
|
+
)
|
|
185
|
+
sid = response.session_id
|
|
186
|
+
console.print(f"[green]agent>[/green] {response.message}\n")
|
|
187
|
+
|
|
188
|
+
asyncio.run(run())
|
|
189
|
+
console.print(Markdown("Demo complete. Try YAML adapter with examples/tenant-kit-adapter/adapter.yaml"))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _default_abilities(adapter: str) -> list[str] | None:
|
|
193
|
+
if adapter == "mock":
|
|
194
|
+
return ["workspaces:read", "workspaces:write", "team:invite"]
|
|
195
|
+
if adapter == "yaml":
|
|
196
|
+
return ["workspaces:read", "workspaces:write", "team:invite"]
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
app()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from api_operator.tools.schema import tool_parameters_schema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class ToolResult:
|
|
13
|
+
ok: bool
|
|
14
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
error: str | None = None
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> dict[str, Any]:
|
|
18
|
+
payload: dict[str, Any] = {"ok": self.ok, "data": self.data}
|
|
19
|
+
if self.error:
|
|
20
|
+
payload["error"] = self.error
|
|
21
|
+
return payload
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class Tool:
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
handler: Callable[..., ToolResult | Awaitable[ToolResult]]
|
|
29
|
+
parameters: dict[str, type | str] = field(default_factory=dict)
|
|
30
|
+
required: list[str] = field(default_factory=list)
|
|
31
|
+
dangerous: bool = False
|
|
32
|
+
requires_ability: str | None = None
|
|
33
|
+
|
|
34
|
+
def schema(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"name": self.name,
|
|
37
|
+
"description": self.description,
|
|
38
|
+
"parameters": tool_parameters_schema(self.parameters, self.required),
|
|
39
|
+
"dangerous": self.dangerous,
|
|
40
|
+
"requires_ability": self.requires_ability,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async def run(self, **kwargs: Any) -> ToolResult:
|
|
44
|
+
result = self.handler(**kwargs)
|
|
45
|
+
if inspect.isawaitable(result):
|
|
46
|
+
result = await result
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ToolRegistry:
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self._tools: dict[str, Tool] = {}
|
|
53
|
+
|
|
54
|
+
def register(self, tool: Tool) -> None:
|
|
55
|
+
if tool.name in self._tools:
|
|
56
|
+
raise ValueError(f"Tool already registered: {tool.name}")
|
|
57
|
+
self._tools[tool.name] = tool
|
|
58
|
+
|
|
59
|
+
def get(self, name: str) -> Tool | None:
|
|
60
|
+
return self._tools.get(name)
|
|
61
|
+
|
|
62
|
+
def list_tools(self) -> list[Tool]:
|
|
63
|
+
return list(self._tools.values())
|
|
64
|
+
|
|
65
|
+
def schemas(self) -> list[dict[str, Any]]:
|
|
66
|
+
return [tool.schema() for tool in self.list_tools()]
|
|
67
|
+
|
|
68
|
+
def names(self) -> list[str]:
|
|
69
|
+
return list(self._tools.keys())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_TYPE_MAP: dict[type, str] = {
|
|
7
|
+
str: "string",
|
|
8
|
+
int: "integer",
|
|
9
|
+
float: "number",
|
|
10
|
+
bool: "boolean",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _json_type(python_type: type | str) -> str:
|
|
15
|
+
if isinstance(python_type, str):
|
|
16
|
+
return python_type
|
|
17
|
+
origin = get_origin(python_type)
|
|
18
|
+
if origin is list:
|
|
19
|
+
return "array"
|
|
20
|
+
return _TYPE_MAP.get(python_type, "string")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def tool_parameters_schema(
|
|
24
|
+
parameters: dict[str, type | str],
|
|
25
|
+
required: list[str],
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
properties: dict[str, Any] = {}
|
|
28
|
+
for name, param_type in parameters.items():
|
|
29
|
+
properties[name] = {"type": _json_type(param_type)}
|
|
30
|
+
return {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": properties,
|
|
33
|
+
"required": required,
|
|
34
|
+
}
|