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.
Files changed (49) hide show
  1. skillware_bench/__init__.py +62 -0
  2. skillware_bench/cli.py +639 -0
  3. skillware_bench/core/adapters/__init__.py +50 -0
  4. skillware_bench/core/adapters/anthropic_adapter.py +166 -0
  5. skillware_bench/core/adapters/base.py +182 -0
  6. skillware_bench/core/adapters/factory.py +140 -0
  7. skillware_bench/core/adapters/gemini_adapter.py +179 -0
  8. skillware_bench/core/adapters/mistral_adapter.py +136 -0
  9. skillware_bench/core/adapters/mock_adapter.py +100 -0
  10. skillware_bench/core/adapters/ollama_adapter.py +137 -0
  11. skillware_bench/core/adapters/openai_adapter.py +164 -0
  12. skillware_bench/core/adapters/openrouter_adapter.py +446 -0
  13. skillware_bench/core/adapters/registry.py +8 -0
  14. skillware_bench/core/agent_loop.py +383 -0
  15. skillware_bench/core/evaluators.py +246 -0
  16. skillware_bench/core/models.py +96 -0
  17. skillware_bench/core/openrouter_engine.py +357 -0
  18. skillware_bench/core/perturbations.py +130 -0
  19. skillware_bench/core/provenance.py +371 -0
  20. skillware_bench/core/runner.py +147 -0
  21. skillware_bench/core/statistics.py +123 -0
  22. skillware_bench/dashboard/__init__.py +8 -0
  23. skillware_bench/dashboard/generator.py +248 -0
  24. skillware_bench/fixtures/__init__.py +9 -0
  25. skillware_bench/fixtures/mock_companies_registry.py +419 -0
  26. skillware_bench/fixtures/mock_inbox_registry.py +228 -0
  27. skillware_bench/infra/__init__.py +27 -0
  28. skillware_bench/infra/companies_house_live.py +228 -0
  29. skillware_bench/infra/disposable_mailbox.py +210 -0
  30. skillware_bench/infra/evm_testnet_fork.py +179 -0
  31. skillware_bench/infra/hermetic_mock_server.py +354 -0
  32. skillware_bench/infra/wallet_screening_live.py +143 -0
  33. skillware_bench/suites/__init__.py +33 -0
  34. skillware_bench/suites/chained_pipeline_crucible.py +177 -0
  35. skillware_bench/suites/companies_house_crucible.py +187 -0
  36. skillware_bench/suites/constitution_crucible.py +171 -0
  37. skillware_bench/suites/context_drift_crucible.py +254 -0
  38. skillware_bench/suites/distraction_registry.py +249 -0
  39. skillware_bench/suites/gmail_crucible.py +207 -0
  40. skillware_bench/suites/prompt_injection_crucible.py +263 -0
  41. skillware_bench/suites/wallet_screening_crucible.py +167 -0
  42. skillware_bench/viz/__init__.py +8 -0
  43. skillware_bench/viz/generator.py +179 -0
  44. skillware_bench-0.3.2.dist-info/METADATA +287 -0
  45. skillware_bench-0.3.2.dist-info/RECORD +49 -0
  46. skillware_bench-0.3.2.dist-info/WHEEL +5 -0
  47. skillware_bench-0.3.2.dist-info/entry_points.txt +2 -0
  48. skillware_bench-0.3.2.dist-info/licenses/LICENSE +21 -0
  49. 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
+ )