skillware-bench 0.3.2__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.
- skillware_bench/__init__.py +62 -0
- skillware_bench/cli.py +639 -0
- skillware_bench/core/adapters/__init__.py +50 -0
- skillware_bench/core/adapters/anthropic_adapter.py +166 -0
- skillware_bench/core/adapters/base.py +182 -0
- skillware_bench/core/adapters/factory.py +140 -0
- skillware_bench/core/adapters/gemini_adapter.py +179 -0
- skillware_bench/core/adapters/mistral_adapter.py +136 -0
- skillware_bench/core/adapters/mock_adapter.py +100 -0
- skillware_bench/core/adapters/ollama_adapter.py +137 -0
- skillware_bench/core/adapters/openai_adapter.py +164 -0
- skillware_bench/core/adapters/openrouter_adapter.py +446 -0
- skillware_bench/core/adapters/registry.py +8 -0
- skillware_bench/core/agent_loop.py +383 -0
- skillware_bench/core/evaluators.py +246 -0
- skillware_bench/core/models.py +96 -0
- skillware_bench/core/openrouter_engine.py +357 -0
- skillware_bench/core/perturbations.py +130 -0
- skillware_bench/core/provenance.py +371 -0
- skillware_bench/core/runner.py +147 -0
- skillware_bench/core/statistics.py +123 -0
- skillware_bench/dashboard/__init__.py +8 -0
- skillware_bench/dashboard/generator.py +248 -0
- skillware_bench/fixtures/__init__.py +9 -0
- skillware_bench/fixtures/mock_companies_registry.py +419 -0
- skillware_bench/fixtures/mock_inbox_registry.py +228 -0
- skillware_bench/infra/__init__.py +27 -0
- skillware_bench/infra/companies_house_live.py +228 -0
- skillware_bench/infra/disposable_mailbox.py +210 -0
- skillware_bench/infra/evm_testnet_fork.py +179 -0
- skillware_bench/infra/hermetic_mock_server.py +354 -0
- skillware_bench/infra/wallet_screening_live.py +143 -0
- skillware_bench/suites/__init__.py +33 -0
- skillware_bench/suites/chained_pipeline_crucible.py +177 -0
- skillware_bench/suites/companies_house_crucible.py +187 -0
- skillware_bench/suites/constitution_crucible.py +171 -0
- skillware_bench/suites/context_drift_crucible.py +254 -0
- skillware_bench/suites/distraction_registry.py +249 -0
- skillware_bench/suites/gmail_crucible.py +207 -0
- skillware_bench/suites/prompt_injection_crucible.py +263 -0
- skillware_bench/suites/wallet_screening_crucible.py +167 -0
- skillware_bench/viz/__init__.py +8 -0
- skillware_bench/viz/generator.py +179 -0
- skillware_bench-0.3.2.dist-info/METADATA +287 -0
- skillware_bench-0.3.2.dist-info/RECORD +49 -0
- skillware_bench-0.3.2.dist-info/WHEEL +5 -0
- skillware_bench-0.3.2.dist-info/entry_points.txt +2 -0
- skillware_bench-0.3.2.dist-info/licenses/LICENSE +21 -0
- skillware_bench-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Mistral AI model adapter (Mistral Large, Codestral)."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from skillware_bench.core.adapters.base import (
|
|
12
|
+
BaseModelAdapter,
|
|
13
|
+
ModelResponse,
|
|
14
|
+
TokenUsage,
|
|
15
|
+
ToolCallRequest,
|
|
16
|
+
execute_with_retry,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from skillware.core.loader import SkillLoader
|
|
21
|
+
except ImportError:
|
|
22
|
+
SkillLoader = None # type: ignore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MistralAdapter(BaseModelAdapter):
|
|
26
|
+
"""Adapter for Mistral AI frontier models using the official mistralai SDK."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
model_name: str = "mistral-large-latest",
|
|
31
|
+
api_key: Optional[str] = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(model_name=model_name, api_key=api_key, **kwargs)
|
|
35
|
+
self.api_key = api_key or os.environ.get("MISTRAL_API_KEY")
|
|
36
|
+
self._client = None
|
|
37
|
+
|
|
38
|
+
def _get_client(self):
|
|
39
|
+
if self._client is None:
|
|
40
|
+
try:
|
|
41
|
+
from mistralai import Mistral
|
|
42
|
+
|
|
43
|
+
self._client = Mistral(api_key=self.api_key)
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
"mistralai package is required for MistralAdapter. Run: pip install mistralai"
|
|
47
|
+
)
|
|
48
|
+
return self._client
|
|
49
|
+
|
|
50
|
+
def format_tools(self, tool_definitions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
51
|
+
formatted: List[Dict[str, Any]] = []
|
|
52
|
+
for tool in tool_definitions:
|
|
53
|
+
if "function" in tool and tool.get("type") == "function":
|
|
54
|
+
formatted.append(tool)
|
|
55
|
+
elif "manifest" in tool and SkillLoader is not None:
|
|
56
|
+
formatted.append(SkillLoader.to_openai_tool(tool))
|
|
57
|
+
else:
|
|
58
|
+
name = tool.get("name", "unknown_tool")
|
|
59
|
+
desc = tool.get("description", "")
|
|
60
|
+
params = tool.get("parameters", {})
|
|
61
|
+
formatted.append(
|
|
62
|
+
{
|
|
63
|
+
"type": "function",
|
|
64
|
+
"function": {
|
|
65
|
+
"name": name,
|
|
66
|
+
"description": desc,
|
|
67
|
+
"parameters": params,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return formatted
|
|
72
|
+
|
|
73
|
+
def chat(
|
|
74
|
+
self,
|
|
75
|
+
messages: List[Dict[str, Any]],
|
|
76
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
77
|
+
system_prompt: Optional[str] = None,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> ModelResponse:
|
|
80
|
+
client = self._get_client()
|
|
81
|
+
|
|
82
|
+
chat_messages = list(messages)
|
|
83
|
+
if system_prompt and not any(m.get("role") == "system" for m in chat_messages):
|
|
84
|
+
chat_messages.insert(0, {"role": "system", "content": system_prompt})
|
|
85
|
+
|
|
86
|
+
payload: Dict[str, Any] = {
|
|
87
|
+
"model": self.model_name,
|
|
88
|
+
"messages": chat_messages,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if self.temperature is not None:
|
|
92
|
+
payload["temperature"] = self.temperature
|
|
93
|
+
if self.max_tokens:
|
|
94
|
+
payload["max_tokens"] = self.max_tokens
|
|
95
|
+
if tools:
|
|
96
|
+
payload["tools"] = self.format_tools(tools)
|
|
97
|
+
|
|
98
|
+
t0 = time.perf_counter()
|
|
99
|
+
|
|
100
|
+
def _call_api():
|
|
101
|
+
return client.chat.complete(**payload)
|
|
102
|
+
|
|
103
|
+
response = execute_with_retry(_call_api)
|
|
104
|
+
latency_ms = (time.perf_counter() - t0) * 1000.0
|
|
105
|
+
|
|
106
|
+
choice = response.choices[0]
|
|
107
|
+
content = choice.message.content or ""
|
|
108
|
+
|
|
109
|
+
tool_calls: List[ToolCallRequest] = []
|
|
110
|
+
if getattr(choice.message, "tool_calls", None):
|
|
111
|
+
for tc in choice.message.tool_calls:
|
|
112
|
+
fn_name = tc.function.name
|
|
113
|
+
try:
|
|
114
|
+
args = json.loads(tc.function.arguments) if tc.function.arguments else {}
|
|
115
|
+
except (json.JSONDecodeError, TypeError):
|
|
116
|
+
args = tc.function.arguments if isinstance(tc.function.arguments, dict) else {}
|
|
117
|
+
tool_calls.append(
|
|
118
|
+
ToolCallRequest(name=fn_name, arguments=args, id=tc.id or fn_name)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
usage = TokenUsage(
|
|
122
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
123
|
+
completion_tokens=response.usage.completion_tokens,
|
|
124
|
+
total_tokens=response.usage.total_tokens,
|
|
125
|
+
)
|
|
126
|
+
self.record_usage(usage)
|
|
127
|
+
|
|
128
|
+
return ModelResponse(
|
|
129
|
+
content=content,
|
|
130
|
+
tool_calls=tool_calls,
|
|
131
|
+
usage=usage,
|
|
132
|
+
latency_ms=latency_ms,
|
|
133
|
+
ttft_ms=latency_ms * 0.40,
|
|
134
|
+
model=self.model_name,
|
|
135
|
+
raw=response,
|
|
136
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Mock model adapter for deterministic offline CI, unit tests, and fault simulation."""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from skillware_bench.core.adapters.base import (
|
|
9
|
+
BaseModelAdapter,
|
|
10
|
+
ModelResponse,
|
|
11
|
+
TokenUsage,
|
|
12
|
+
ToolCallRequest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimulatedRateLimitError(Exception):
|
|
17
|
+
"""Simulated 429 Too Many Requests error for retry testing."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SimulatedServerError(Exception):
|
|
21
|
+
"""Simulated 503 Service Unavailable error for retry testing."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MockModelAdapter(BaseModelAdapter):
|
|
25
|
+
"""Deterministic scriptable adapter for hermetic testing and error simulation."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
model_name: str = "golden-target",
|
|
30
|
+
scripted_responses: Optional[List[ModelResponse]] = None,
|
|
31
|
+
simulated_failures_before_success: int = 0,
|
|
32
|
+
failure_exception_type: type = SimulatedRateLimitError,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(model_name=model_name, **kwargs)
|
|
36
|
+
self.scripted_responses = list(scripted_responses) if scripted_responses else []
|
|
37
|
+
self.simulated_failures_remaining = simulated_failures_before_success
|
|
38
|
+
self.failure_exception_type = failure_exception_type
|
|
39
|
+
self.call_history: List[Dict[str, Any]] = []
|
|
40
|
+
self.step_counter = 0
|
|
41
|
+
|
|
42
|
+
def add_response(self, response: ModelResponse) -> None:
|
|
43
|
+
self.scripted_responses.append(response)
|
|
44
|
+
|
|
45
|
+
def chat(
|
|
46
|
+
self,
|
|
47
|
+
messages: List[Dict[str, Any]],
|
|
48
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
49
|
+
system_prompt: Optional[str] = None,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> ModelResponse:
|
|
52
|
+
self.call_history.append(
|
|
53
|
+
{
|
|
54
|
+
"messages": messages,
|
|
55
|
+
"tools": tools,
|
|
56
|
+
"system_prompt": system_prompt,
|
|
57
|
+
"kwargs": kwargs,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if self.simulated_failures_remaining > 0:
|
|
62
|
+
self.simulated_failures_remaining -= 1
|
|
63
|
+
raise self.failure_exception_type("Simulated provider rate limit (429) backoff trigger")
|
|
64
|
+
|
|
65
|
+
if self.scripted_responses and self.step_counter < len(self.scripted_responses):
|
|
66
|
+
resp = self.scripted_responses[self.step_counter]
|
|
67
|
+
self.step_counter += 1
|
|
68
|
+
self.record_usage(resp.usage)
|
|
69
|
+
return resp
|
|
70
|
+
|
|
71
|
+
# Default fallback response
|
|
72
|
+
self.step_counter += 1
|
|
73
|
+
default_usage = TokenUsage(prompt_tokens=150, completion_tokens=30, total_tokens=180)
|
|
74
|
+
self.record_usage(default_usage)
|
|
75
|
+
|
|
76
|
+
tool_calls: List[ToolCallRequest] = []
|
|
77
|
+
if tools and len(tools) > 0:
|
|
78
|
+
first_tool = tools[0]
|
|
79
|
+
name = (
|
|
80
|
+
first_tool.get("function", {}).get("name")
|
|
81
|
+
or first_tool.get("name")
|
|
82
|
+
or first_tool.get("manifest", {}).get("name")
|
|
83
|
+
or "execute_action"
|
|
84
|
+
)
|
|
85
|
+
tool_calls.append(
|
|
86
|
+
ToolCallRequest(
|
|
87
|
+
name=name,
|
|
88
|
+
arguments={"status": "mock_arg"},
|
|
89
|
+
id=f"call_{self.step_counter}",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return ModelResponse(
|
|
94
|
+
content=f"Mock model response turn {self.step_counter}",
|
|
95
|
+
tool_calls=tool_calls,
|
|
96
|
+
usage=default_usage,
|
|
97
|
+
latency_ms=120.0,
|
|
98
|
+
ttft_ms=50.0,
|
|
99
|
+
model=self.model_name,
|
|
100
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Ollama local sovereign model adapter (Llama 3.2, Qwen 2.5)."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from skillware_bench.core.adapters.base import (
|
|
13
|
+
BaseModelAdapter,
|
|
14
|
+
ModelResponse,
|
|
15
|
+
TokenUsage,
|
|
16
|
+
ToolCallRequest,
|
|
17
|
+
execute_with_retry,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from skillware.core.loader import SkillLoader
|
|
22
|
+
except ImportError:
|
|
23
|
+
SkillLoader = None # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OllamaAdapter(BaseModelAdapter):
|
|
27
|
+
"""Adapter for local sovereign models running via Ollama API."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
model_name: str = "llama3.2",
|
|
32
|
+
host: Optional[str] = None,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(model_name=model_name, **kwargs)
|
|
36
|
+
self.host = (host or os.environ.get("OLLAMA_HOST") or "http://localhost:11434").rstrip("/")
|
|
37
|
+
|
|
38
|
+
def format_tools(self, tool_definitions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
39
|
+
formatted: List[Dict[str, Any]] = []
|
|
40
|
+
for tool in tool_definitions:
|
|
41
|
+
if "function" in tool and tool.get("type") == "function":
|
|
42
|
+
formatted.append(tool)
|
|
43
|
+
elif "manifest" in tool and SkillLoader is not None:
|
|
44
|
+
formatted.append(SkillLoader.to_openai_tool(tool))
|
|
45
|
+
else:
|
|
46
|
+
name = tool.get("name", "unknown_tool")
|
|
47
|
+
desc = tool.get("description", "")
|
|
48
|
+
params = tool.get("parameters", {})
|
|
49
|
+
formatted.append(
|
|
50
|
+
{
|
|
51
|
+
"type": "function",
|
|
52
|
+
"function": {
|
|
53
|
+
"name": name,
|
|
54
|
+
"description": desc,
|
|
55
|
+
"parameters": params,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
return formatted
|
|
60
|
+
|
|
61
|
+
def chat(
|
|
62
|
+
self,
|
|
63
|
+
messages: List[Dict[str, Any]],
|
|
64
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
65
|
+
system_prompt: Optional[str] = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> ModelResponse:
|
|
68
|
+
url = f"{self.host}/api/chat"
|
|
69
|
+
|
|
70
|
+
chat_messages = list(messages)
|
|
71
|
+
if system_prompt and not any(m.get("role") == "system" for m in chat_messages):
|
|
72
|
+
chat_messages.insert(0, {"role": "system", "content": system_prompt})
|
|
73
|
+
|
|
74
|
+
payload: Dict[str, Any] = {
|
|
75
|
+
"model": self.model_name,
|
|
76
|
+
"messages": chat_messages,
|
|
77
|
+
"stream": False,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
options: Dict[str, Any] = {}
|
|
81
|
+
if self.temperature is not None:
|
|
82
|
+
options["temperature"] = self.temperature
|
|
83
|
+
if self.max_tokens:
|
|
84
|
+
options["num_predict"] = self.max_tokens
|
|
85
|
+
if options:
|
|
86
|
+
payload["options"] = options
|
|
87
|
+
|
|
88
|
+
if tools:
|
|
89
|
+
payload["tools"] = self.format_tools(tools)
|
|
90
|
+
|
|
91
|
+
t0 = time.perf_counter()
|
|
92
|
+
|
|
93
|
+
def _call_api():
|
|
94
|
+
resp = requests.post(url, json=payload, timeout=self.timeout_seconds)
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
return resp.json()
|
|
97
|
+
|
|
98
|
+
data = execute_with_retry(
|
|
99
|
+
_call_api,
|
|
100
|
+
retryable_exceptions=(requests.RequestException,),
|
|
101
|
+
)
|
|
102
|
+
latency_ms = (time.perf_counter() - t0) * 1000.0
|
|
103
|
+
|
|
104
|
+
message_data = data.get("message", {})
|
|
105
|
+
content = message_data.get("content", "")
|
|
106
|
+
|
|
107
|
+
tool_calls: List[ToolCallRequest] = []
|
|
108
|
+
raw_tools = message_data.get("tool_calls", [])
|
|
109
|
+
for tc in raw_tools:
|
|
110
|
+
fn = tc.get("function", {})
|
|
111
|
+
fn_name = fn.get("name", "unknown")
|
|
112
|
+
args = fn.get("arguments", {})
|
|
113
|
+
if isinstance(args, str):
|
|
114
|
+
try:
|
|
115
|
+
args = json.loads(args)
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
args = {"_raw": args}
|
|
118
|
+
tool_calls.append(ToolCallRequest(name=fn_name, arguments=args, id=fn_name))
|
|
119
|
+
|
|
120
|
+
prompt_tokens = data.get("prompt_eval_count", 0) or 0
|
|
121
|
+
completion_tokens = data.get("eval_count", 0) or 0
|
|
122
|
+
usage = TokenUsage(
|
|
123
|
+
prompt_tokens=prompt_tokens,
|
|
124
|
+
completion_tokens=completion_tokens,
|
|
125
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
126
|
+
)
|
|
127
|
+
self.record_usage(usage)
|
|
128
|
+
|
|
129
|
+
return ModelResponse(
|
|
130
|
+
content=content,
|
|
131
|
+
tool_calls=tool_calls,
|
|
132
|
+
usage=usage,
|
|
133
|
+
latency_ms=latency_ms,
|
|
134
|
+
ttft_ms=latency_ms * 0.50,
|
|
135
|
+
model=self.model_name,
|
|
136
|
+
raw=data,
|
|
137
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""OpenAI model adapter (GPT-4o, o3-mini, o1)."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from skillware_bench.core.adapters.base import (
|
|
12
|
+
BaseModelAdapter,
|
|
13
|
+
ModelResponse,
|
|
14
|
+
TokenUsage,
|
|
15
|
+
ToolCallRequest,
|
|
16
|
+
execute_with_retry,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from skillware.core.loader import SkillLoader
|
|
21
|
+
except ImportError:
|
|
22
|
+
SkillLoader = None # type: ignore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenAIAdapter(BaseModelAdapter):
|
|
26
|
+
"""Adapter for OpenAI models supporting chat completions and function calling."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
model_name: str = "gpt-4o",
|
|
31
|
+
api_key: Optional[str] = None,
|
|
32
|
+
base_url: Optional[str] = None,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(model_name=model_name, api_key=api_key, **kwargs)
|
|
36
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
|
|
37
|
+
self.base_url = base_url or os.environ.get("OPENAI_BASE_URL")
|
|
38
|
+
self._client = None
|
|
39
|
+
|
|
40
|
+
def _get_client(self):
|
|
41
|
+
if self._client is None:
|
|
42
|
+
try:
|
|
43
|
+
import openai
|
|
44
|
+
|
|
45
|
+
self._client = openai.OpenAI(
|
|
46
|
+
api_key=self.api_key,
|
|
47
|
+
base_url=self.base_url,
|
|
48
|
+
timeout=self.timeout_seconds,
|
|
49
|
+
)
|
|
50
|
+
except ImportError:
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"openai package is required to use OpenAIAdapter. Run: pip install openai"
|
|
53
|
+
)
|
|
54
|
+
return self._client
|
|
55
|
+
|
|
56
|
+
def format_tools(self, tool_definitions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
57
|
+
formatted: List[Dict[str, Any]] = []
|
|
58
|
+
for tool in tool_definitions:
|
|
59
|
+
if "function" in tool and tool.get("type") == "function":
|
|
60
|
+
formatted.append(tool)
|
|
61
|
+
elif "manifest" in tool and SkillLoader is not None:
|
|
62
|
+
formatted.append(SkillLoader.to_openai_tool(tool))
|
|
63
|
+
else:
|
|
64
|
+
name = tool.get("name", "unknown_tool")
|
|
65
|
+
desc = tool.get("description", "")
|
|
66
|
+
params = tool.get("parameters", {})
|
|
67
|
+
formatted.append(
|
|
68
|
+
{
|
|
69
|
+
"type": "function",
|
|
70
|
+
"function": {
|
|
71
|
+
"name": name,
|
|
72
|
+
"description": desc,
|
|
73
|
+
"parameters": params,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return formatted
|
|
78
|
+
|
|
79
|
+
def chat(
|
|
80
|
+
self,
|
|
81
|
+
messages: List[Dict[str, Any]],
|
|
82
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
83
|
+
system_prompt: Optional[str] = None,
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> ModelResponse:
|
|
86
|
+
client = self._get_client()
|
|
87
|
+
|
|
88
|
+
chat_messages = list(messages)
|
|
89
|
+
if system_prompt and not any(m.get("role") == "system" for m in chat_messages):
|
|
90
|
+
role = "developer" if ("o1" in self.model_name or "o3" in self.model_name) else "system"
|
|
91
|
+
chat_messages.insert(0, {"role": role, "content": system_prompt})
|
|
92
|
+
|
|
93
|
+
payload: Dict[str, Any] = {
|
|
94
|
+
"model": self.model_name,
|
|
95
|
+
"messages": chat_messages,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Temperature is not supported on o1/o3-mini in certain API versions
|
|
99
|
+
if not ("o1" in self.model_name or "o3" in self.model_name):
|
|
100
|
+
payload["temperature"] = self.temperature
|
|
101
|
+
|
|
102
|
+
if self.max_tokens:
|
|
103
|
+
payload["max_tokens"] = self.max_tokens
|
|
104
|
+
|
|
105
|
+
if tools:
|
|
106
|
+
payload["tools"] = self.format_tools(tools)
|
|
107
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
108
|
+
|
|
109
|
+
t0 = time.perf_counter()
|
|
110
|
+
|
|
111
|
+
def _call_api():
|
|
112
|
+
return client.chat.completions.create(**payload)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
import openai
|
|
116
|
+
|
|
117
|
+
retry_types = (
|
|
118
|
+
openai.RateLimitError,
|
|
119
|
+
openai.APIConnectionError,
|
|
120
|
+
openai.InternalServerError,
|
|
121
|
+
)
|
|
122
|
+
except Exception:
|
|
123
|
+
retry_types = (Exception,)
|
|
124
|
+
|
|
125
|
+
response = execute_with_retry(_call_api, retryable_exceptions=retry_types)
|
|
126
|
+
latency_ms = (time.perf_counter() - t0) * 1000.0
|
|
127
|
+
|
|
128
|
+
choice = response.choices[0]
|
|
129
|
+
content = choice.message.content or ""
|
|
130
|
+
|
|
131
|
+
tool_calls: List[ToolCallRequest] = []
|
|
132
|
+
if getattr(choice.message, "tool_calls", None):
|
|
133
|
+
for tc in choice.message.tool_calls:
|
|
134
|
+
fn_name = tc.function.name
|
|
135
|
+
try:
|
|
136
|
+
args = json.loads(tc.function.arguments) if tc.function.arguments else {}
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
args = {"_raw": tc.function.arguments}
|
|
139
|
+
tool_calls.append(ToolCallRequest(name=fn_name, arguments=args, id=tc.id))
|
|
140
|
+
|
|
141
|
+
cached_tokens = 0
|
|
142
|
+
if (
|
|
143
|
+
hasattr(response.usage, "prompt_tokens_details")
|
|
144
|
+
and response.usage.prompt_tokens_details
|
|
145
|
+
):
|
|
146
|
+
cached_tokens = getattr(response.usage.prompt_tokens_details, "cached_tokens", 0) or 0
|
|
147
|
+
|
|
148
|
+
usage = TokenUsage(
|
|
149
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
150
|
+
completion_tokens=response.usage.completion_tokens,
|
|
151
|
+
cached_tokens=cached_tokens,
|
|
152
|
+
total_tokens=response.usage.total_tokens,
|
|
153
|
+
)
|
|
154
|
+
self.record_usage(usage)
|
|
155
|
+
|
|
156
|
+
return ModelResponse(
|
|
157
|
+
content=content,
|
|
158
|
+
tool_calls=tool_calls,
|
|
159
|
+
usage=usage,
|
|
160
|
+
latency_ms=latency_ms,
|
|
161
|
+
ttft_ms=latency_ms * 0.45,
|
|
162
|
+
model=self.model_name,
|
|
163
|
+
raw=response,
|
|
164
|
+
)
|