sectum-ai-adapters 0.1.1__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.
- sectum_ai_adapters-0.1.1/.gitignore +45 -0
- sectum_ai_adapters-0.1.1/PKG-INFO +96 -0
- sectum_ai_adapters-0.1.1/README.md +31 -0
- sectum_ai_adapters-0.1.1/pyproject.toml +63 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/__init__.py +92 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/__init__.py +10 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/_anthropic_tooluse_live.py +180 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/_openai_assistants_live.py +149 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/anthropic_tooluse.py +162 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/autogen.py +292 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/crewai.py +233 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/http.py +64 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/langgraph.py +179 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/agent/openai_assistants.py +173 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/base.py +488 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/cache/__init__.py +6 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/cache/redis.py +90 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/fakes.py +909 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/mcp/__init__.py +8 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/mcp/client.py +79 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/mcp/http.py +90 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/model/__init__.py +5 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/model/_huggingface_live.py +187 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/model/huggingface.py +256 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/__init__.py +7 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/datadog.py +205 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/helicone.py +184 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/langfuse.py +133 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/langsmith.py +85 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/otel.py +250 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/observability/phoenix.py +69 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/py.typed +0 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/rag/__init__.py +6 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/rag/http.py +77 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/rag/langchain.py +182 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/__init__.py +6 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/azure_search.py +237 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/chroma.py +132 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/milvus.py +197 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/opensearch.py +210 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/pgvector.py +140 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/pinecone.py +169 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/qdrant.py +203 -0
- sectum_ai_adapters-0.1.1/src/sectum_ai/adapters/vector/weaviate.py +176 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
|
|
8
|
+
# Builds / distributions
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.whl
|
|
12
|
+
|
|
13
|
+
# mkdocs build output
|
|
14
|
+
site/
|
|
15
|
+
|
|
16
|
+
# uv / virtual environments
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
|
|
20
|
+
# Tooling caches
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
.coverage.*
|
|
26
|
+
coverage.xml
|
|
27
|
+
htmlcov/
|
|
28
|
+
|
|
29
|
+
# Editors / OS
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Example run artifacts (generated by examples/*/run.sh, incl. the
|
|
36
|
+
# out-residual/ workdir from the docs/samples regeneration recipe)
|
|
37
|
+
examples/*/out/
|
|
38
|
+
examples/*/out-residual/
|
|
39
|
+
|
|
40
|
+
# Sectum CLI default workdir (generated by seed/probe/report; not source)
|
|
41
|
+
.sectum-ai/
|
|
42
|
+
examples/*/.sectum-ai/
|
|
43
|
+
|
|
44
|
+
# Project-local engineering spec (not shared)
|
|
45
|
+
CLAUDE.md
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sectum-ai-adapters
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Sectum AI - adapters connecting the substrate and probes to real systems.
|
|
5
|
+
Project-URL: Homepage, https://sectum.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/sectum-ai/sectum-ai
|
|
7
|
+
Author: Sectum AI
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: adapters,ai-security,multi-tenant,verification
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: pydantic>=2.9
|
|
17
|
+
Requires-Dist: sectum-ai-spec
|
|
18
|
+
Provides-Extra: anthropic-tooluse
|
|
19
|
+
Requires-Dist: anthropic>=0.40; extra == 'anthropic-tooluse'
|
|
20
|
+
Provides-Extra: autogen
|
|
21
|
+
Requires-Dist: pyautogen<0.3,>=0.2; extra == 'autogen'
|
|
22
|
+
Provides-Extra: azure-search
|
|
23
|
+
Requires-Dist: azure-core>=1.30; extra == 'azure-search'
|
|
24
|
+
Requires-Dist: azure-search-documents>=11.4; extra == 'azure-search'
|
|
25
|
+
Provides-Extra: chroma
|
|
26
|
+
Requires-Dist: chromadb-client>=0.5; extra == 'chroma'
|
|
27
|
+
Provides-Extra: crewai
|
|
28
|
+
Requires-Dist: crewai>=0.80; extra == 'crewai'
|
|
29
|
+
Provides-Extra: huggingface
|
|
30
|
+
Requires-Dist: accelerate>=1; extra == 'huggingface'
|
|
31
|
+
Requires-Dist: peft>=0.13; extra == 'huggingface'
|
|
32
|
+
Requires-Dist: torch>=2.4; extra == 'huggingface'
|
|
33
|
+
Requires-Dist: transformers>=4.45; extra == 'huggingface'
|
|
34
|
+
Provides-Extra: langfuse
|
|
35
|
+
Requires-Dist: langfuse<4,>=3; extra == 'langfuse'
|
|
36
|
+
Provides-Extra: langgraph
|
|
37
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langgraph'
|
|
38
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
39
|
+
Provides-Extra: langsmith
|
|
40
|
+
Requires-Dist: langsmith>=0.1; extra == 'langsmith'
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp>=1; extra == 'mcp'
|
|
43
|
+
Provides-Extra: milvus
|
|
44
|
+
Requires-Dist: pymilvus>=2.4; extra == 'milvus'
|
|
45
|
+
Provides-Extra: openai-assistants
|
|
46
|
+
Requires-Dist: openai>=1.50; extra == 'openai-assistants'
|
|
47
|
+
Provides-Extra: opensearch
|
|
48
|
+
Requires-Dist: opensearch-py>=2.4; extra == 'opensearch'
|
|
49
|
+
Provides-Extra: pgvector
|
|
50
|
+
Requires-Dist: psycopg[binary]>=3.3.4; extra == 'pgvector'
|
|
51
|
+
Provides-Extra: phoenix
|
|
52
|
+
Requires-Dist: arize-phoenix-client>=2; extra == 'phoenix'
|
|
53
|
+
Requires-Dist: httpx>=0.27; extra == 'phoenix'
|
|
54
|
+
Provides-Extra: pinecone
|
|
55
|
+
Requires-Dist: pinecone>=5; extra == 'pinecone'
|
|
56
|
+
Provides-Extra: qdrant
|
|
57
|
+
Requires-Dist: qdrant-client>=1.12; extra == 'qdrant'
|
|
58
|
+
Provides-Extra: rag-langchain
|
|
59
|
+
Requires-Dist: langchain-core>=0.3; extra == 'rag-langchain'
|
|
60
|
+
Provides-Extra: redis
|
|
61
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
62
|
+
Provides-Extra: weaviate
|
|
63
|
+
Requires-Dist: weaviate-client>=4.9; extra == 'weaviate'
|
|
64
|
+
Description-Content-Type: text/markdown
|
|
65
|
+
|
|
66
|
+
# sectum-ai-adapters
|
|
67
|
+
|
|
68
|
+
The adapter SDK for [Sectum AI](https://github.com/sectum-ai/sectum-ai) —
|
|
69
|
+
the connectors that point the marker substrate and the attack catalog at real
|
|
70
|
+
systems.
|
|
71
|
+
|
|
72
|
+
Every adapter family ships a deterministic in-memory `fake` (used by the unit
|
|
73
|
+
suite and offline runs) plus live backends, each behind a capability-reporting
|
|
74
|
+
interface so probes can declare what they require:
|
|
75
|
+
|
|
76
|
+
- **Vector stores** — pgvector, Chroma, Weaviate, Qdrant, Pinecone, Milvus, OpenSearch, Azure AI Search
|
|
77
|
+
- **RAG pipelines** — generic HTTP, LangChain
|
|
78
|
+
- **Observability** — Langfuse, LangSmith, Phoenix, Helicone, Datadog APM, generic OpenTelemetry
|
|
79
|
+
- **Agents** — LangGraph, AutoGen, CrewAI, OpenAI Assistants, Anthropic tool-use, generic HTTP
|
|
80
|
+
- **MCP** — stdio + streamable-HTTP Model Context Protocol clients
|
|
81
|
+
- **Cache** — Redis
|
|
82
|
+
- **Model** — HuggingFace + PEFT LoRA
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
pip install sectum-ai-adapters
|
|
86
|
+
# live backends are opt-in extras, e.g.:
|
|
87
|
+
pip install "sectum-ai-adapters[pgvector]" # or [redis], [langgraph], [anthropic-tooluse], ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Most users install the umbrella package [`sectum-ai`](https://pypi.org/project/sectum-ai/)
|
|
91
|
+
instead, which pulls this in automatically.
|
|
92
|
+
|
|
93
|
+
- Adapter configuration reference: <https://docs.sectum.ai>
|
|
94
|
+
- Source: <https://github.com/sectum-ai/sectum-ai>
|
|
95
|
+
|
|
96
|
+
Apache-2.0.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sectum-ai-adapters
|
|
2
|
+
|
|
3
|
+
The adapter SDK for [Sectum AI](https://github.com/sectum-ai/sectum-ai) —
|
|
4
|
+
the connectors that point the marker substrate and the attack catalog at real
|
|
5
|
+
systems.
|
|
6
|
+
|
|
7
|
+
Every adapter family ships a deterministic in-memory `fake` (used by the unit
|
|
8
|
+
suite and offline runs) plus live backends, each behind a capability-reporting
|
|
9
|
+
interface so probes can declare what they require:
|
|
10
|
+
|
|
11
|
+
- **Vector stores** — pgvector, Chroma, Weaviate, Qdrant, Pinecone, Milvus, OpenSearch, Azure AI Search
|
|
12
|
+
- **RAG pipelines** — generic HTTP, LangChain
|
|
13
|
+
- **Observability** — Langfuse, LangSmith, Phoenix, Helicone, Datadog APM, generic OpenTelemetry
|
|
14
|
+
- **Agents** — LangGraph, AutoGen, CrewAI, OpenAI Assistants, Anthropic tool-use, generic HTTP
|
|
15
|
+
- **MCP** — stdio + streamable-HTTP Model Context Protocol clients
|
|
16
|
+
- **Cache** — Redis
|
|
17
|
+
- **Model** — HuggingFace + PEFT LoRA
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install sectum-ai-adapters
|
|
21
|
+
# live backends are opt-in extras, e.g.:
|
|
22
|
+
pip install "sectum-ai-adapters[pgvector]" # or [redis], [langgraph], [anthropic-tooluse], ...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Most users install the umbrella package [`sectum-ai`](https://pypi.org/project/sectum-ai/)
|
|
26
|
+
instead, which pulls this in automatically.
|
|
27
|
+
|
|
28
|
+
- Adapter configuration reference: <https://docs.sectum.ai>
|
|
29
|
+
- Source: <https://github.com/sectum-ai/sectum-ai>
|
|
30
|
+
|
|
31
|
+
Apache-2.0.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sectum-ai-adapters"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Sectum AI - adapters connecting the substrate and probes to real systems."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Sectum AI" }]
|
|
13
|
+
keywords = ["ai-security", "multi-tenant", "adapters", "verification"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"sectum-ai-spec",
|
|
23
|
+
# base.py imports pydantic directly (adapter config models); declare it
|
|
24
|
+
# rather than relying on the transitive dependency via sectum-ai-spec (§13).
|
|
25
|
+
"pydantic>=2.9",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
pgvector = ["psycopg[binary]>=3.3.4"]
|
|
30
|
+
chroma = ["chromadb-client>=0.5"]
|
|
31
|
+
weaviate = ["weaviate-client>=4.9"]
|
|
32
|
+
redis = ["redis>=5"]
|
|
33
|
+
# the Phoenix adapter imports httpx directly (observability/phoenix.py); declare
|
|
34
|
+
# it explicitly rather than relying on arize-phoenix-client to pull it in.
|
|
35
|
+
phoenix = ["arize-phoenix-client>=2", "httpx>=0.27"]
|
|
36
|
+
langfuse = ["langfuse>=3,<4"]
|
|
37
|
+
langsmith = ["langsmith>=0.1"]
|
|
38
|
+
mcp = ["mcp>=1"]
|
|
39
|
+
pinecone = ["pinecone>=5"]
|
|
40
|
+
qdrant = ["qdrant-client>=1.12"]
|
|
41
|
+
milvus = ["pymilvus>=2.4"]
|
|
42
|
+
opensearch = ["opensearch-py>=2.4"]
|
|
43
|
+
azure-search = ["azure-search-documents>=11.4", "azure-core>=1.30"]
|
|
44
|
+
langgraph = ["langgraph>=0.2", "langchain-core>=0.3"]
|
|
45
|
+
autogen = ["pyautogen>=0.2,<0.3"]
|
|
46
|
+
crewai = ["crewai>=0.80"]
|
|
47
|
+
huggingface = ["transformers>=4.45", "peft>=0.13", "torch>=2.4", "accelerate>=1"]
|
|
48
|
+
openai-assistants = ["openai>=1.50"]
|
|
49
|
+
anthropic-tooluse = ["anthropic>=0.40"]
|
|
50
|
+
rag-langchain = ["langchain-core>=0.3"]
|
|
51
|
+
# helicone + datadog observability read over their REST APIs with the standard
|
|
52
|
+
# library (no SDK), so they need no extras group; listed here for discoverability.
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://sectum.ai"
|
|
56
|
+
Repository = "https://github.com/sectum-ai/sectum-ai"
|
|
57
|
+
|
|
58
|
+
[tool.uv.sources]
|
|
59
|
+
sectum-ai-spec = { workspace = true }
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
only-include = ["src/sectum_ai"]
|
|
63
|
+
sources = ["src"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Sectum AI adapters: connectors to vector stores, RAG, observability, agents, MCP, and caches.
|
|
2
|
+
|
|
3
|
+
This is the ``sectum_ai.adapters`` namespace package (the engineering spec, section 11).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import version as _dist_version
|
|
8
|
+
|
|
9
|
+
from sectum_ai.adapters.base import (
|
|
10
|
+
Adapter,
|
|
11
|
+
AdapterFamily,
|
|
12
|
+
AdapterRegistry,
|
|
13
|
+
AgentAdapter,
|
|
14
|
+
AgentResult,
|
|
15
|
+
BackupAdapter,
|
|
16
|
+
CacheAdapter,
|
|
17
|
+
Capability,
|
|
18
|
+
EvalSetAdapter,
|
|
19
|
+
MCPAdapter,
|
|
20
|
+
McpResult,
|
|
21
|
+
MemoryAdapter,
|
|
22
|
+
ModelAdapter,
|
|
23
|
+
ObservabilityAdapter,
|
|
24
|
+
RagAnswer,
|
|
25
|
+
RAGPipelineAdapter,
|
|
26
|
+
SearchIndexAdapter,
|
|
27
|
+
TraceHit,
|
|
28
|
+
VectorHit,
|
|
29
|
+
VectorStoreAdapter,
|
|
30
|
+
)
|
|
31
|
+
from sectum_ai.adapters.fakes import (
|
|
32
|
+
FakeAgent,
|
|
33
|
+
FakeBackup,
|
|
34
|
+
FakeCache,
|
|
35
|
+
FakeEvalSet,
|
|
36
|
+
FakeMCP,
|
|
37
|
+
FakeMemory,
|
|
38
|
+
FakeModel,
|
|
39
|
+
FakeObservability,
|
|
40
|
+
FakeRAGPipeline,
|
|
41
|
+
FakeSearchIndex,
|
|
42
|
+
FakeVectorStore,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Adapter",
|
|
47
|
+
"AdapterFamily",
|
|
48
|
+
"AdapterRegistry",
|
|
49
|
+
"AgentAdapter",
|
|
50
|
+
"AgentResult",
|
|
51
|
+
"BackupAdapter",
|
|
52
|
+
"CacheAdapter",
|
|
53
|
+
"Capability",
|
|
54
|
+
"EvalSetAdapter",
|
|
55
|
+
"FakeAgent",
|
|
56
|
+
"FakeBackup",
|
|
57
|
+
"FakeCache",
|
|
58
|
+
"FakeEvalSet",
|
|
59
|
+
"FakeMCP",
|
|
60
|
+
"FakeMemory",
|
|
61
|
+
"FakeModel",
|
|
62
|
+
"FakeObservability",
|
|
63
|
+
"FakeRAGPipeline",
|
|
64
|
+
"FakeSearchIndex",
|
|
65
|
+
"FakeVectorStore",
|
|
66
|
+
"MCPAdapter",
|
|
67
|
+
"McpResult",
|
|
68
|
+
"MemoryAdapter",
|
|
69
|
+
"ModelAdapter",
|
|
70
|
+
"ObservabilityAdapter",
|
|
71
|
+
"RAGPipelineAdapter",
|
|
72
|
+
"RagAnswer",
|
|
73
|
+
"SearchIndexAdapter",
|
|
74
|
+
"TraceHit",
|
|
75
|
+
"VectorHit",
|
|
76
|
+
"VectorStoreAdapter",
|
|
77
|
+
"version",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def version() -> str:
|
|
82
|
+
"""Return the installed ``sectum-ai-adapters`` distribution version.
|
|
83
|
+
|
|
84
|
+
Stamped into ``RunResult.adapter_versions`` so an evidence pack attests the
|
|
85
|
+
version of the *adapters* distribution that produced it, not the core CLI's
|
|
86
|
+
(the engineering spec, section 8.2). Falls back to ``0.0.0+unknown`` in an
|
|
87
|
+
uninstalled / editable tree.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
return _dist_version("sectum-ai-adapters")
|
|
91
|
+
except PackageNotFoundError: # pragma: no cover - only in an uninstalled tree
|
|
92
|
+
return "0.0.0+unknown"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Live agent adapters.
|
|
2
|
+
|
|
3
|
+
Each module here connects to one real agent backend and is imported explicitly
|
|
4
|
+
(for example ``from sectum_ai.adapters.agent.http import HttpAgent``,
|
|
5
|
+
``from sectum_ai.adapters.agent.langgraph import LangGraphAgent``,
|
|
6
|
+
``from sectum_ai.adapters.agent.autogen import AutoGenAgent``,
|
|
7
|
+
``from sectum_ai.adapters.agent.crewai import CrewAIAgent``,
|
|
8
|
+
``from sectum_ai.adapters.agent.openai_assistants import OpenAIAssistantsAgent``, or
|
|
9
|
+
``from sectum_ai.adapters.agent.anthropic_tooluse import AnthropicToolUseAgent``).
|
|
10
|
+
"""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Live Anthropic Messages-API client backing the tool-use adapter.
|
|
2
|
+
|
|
3
|
+
Imported lazily by ``AnthropicToolUseAgent.connect``. The module imports
|
|
4
|
+
``anthropic`` at module load, so a downstream import of
|
|
5
|
+
``sectum_ai.adapters.agent.anthropic_tooluse`` does NOT pull the SDK —
|
|
6
|
+
only construction via ``connect`` does.
|
|
7
|
+
|
|
8
|
+
Implements the ``_AnthropicClient`` protocol the adapter consumes:
|
|
9
|
+
|
|
10
|
+
- ``run_turn`` — calls ``messages.create`` on the supplied conversation
|
|
11
|
+
history; for each ``tool_use`` block in the response, executes the
|
|
12
|
+
registered python callable, appends a ``tool_result`` user message,
|
|
13
|
+
and repeats until ``stop_reason: end_turn``. Returns the final
|
|
14
|
+
assistant text + the list of tool names that fired across the loop.
|
|
15
|
+
|
|
16
|
+
Tool execution: the caller attaches a python callable to each tool
|
|
17
|
+
spec via the ``__sectum_callable__`` attribute. The backend looks the
|
|
18
|
+
callable up by the tool's ``name`` field.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from sectum_ai.spec import AdapterError
|
|
27
|
+
|
|
28
|
+
_MAX_TOOL_USE_ITERATIONS = 16
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LiveAnthropicClient:
|
|
32
|
+
"""Live ``_AnthropicClient`` implementation backed by the Anthropic Python SDK.
|
|
33
|
+
|
|
34
|
+
Holds a single ``anthropic.Anthropic`` client plus a registry of
|
|
35
|
+
the python callables that back each registered tool by name.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
api_key: str | None,
|
|
42
|
+
model: str,
|
|
43
|
+
tools: list[Any],
|
|
44
|
+
max_tokens: int,
|
|
45
|
+
system: str,
|
|
46
|
+
) -> None:
|
|
47
|
+
try:
|
|
48
|
+
from anthropic import Anthropic
|
|
49
|
+
except ImportError as error:
|
|
50
|
+
raise AdapterError(
|
|
51
|
+
"anthropic-tooluse adapter requires the `anthropic` package; "
|
|
52
|
+
"install sectum-ai-adapters[anthropic-tooluse] to enable it"
|
|
53
|
+
) from error
|
|
54
|
+
self._anthropic = Anthropic(api_key=api_key) if api_key else Anthropic()
|
|
55
|
+
self._model = model
|
|
56
|
+
self._max_tokens = max_tokens
|
|
57
|
+
self._system = system
|
|
58
|
+
# The Anthropic tool spec is a dict carrying name / description /
|
|
59
|
+
# input_schema. Strip the python-callable sidecar before passing
|
|
60
|
+
# it to the SDK (the SDK rejects unknown keys on tool specs).
|
|
61
|
+
self._tools: list[dict[str, Any]] = []
|
|
62
|
+
self._tool_targets: dict[str, Any] = {}
|
|
63
|
+
for tool in tools:
|
|
64
|
+
spec = getattr(tool, "__sectum_tool_spec__", None) or tool
|
|
65
|
+
if not isinstance(spec, dict) or "name" not in spec:
|
|
66
|
+
raise AdapterError(
|
|
67
|
+
"each tool must be (or carry) an Anthropic tool-spec dict "
|
|
68
|
+
"with at least a `name` field; got "
|
|
69
|
+
f"{type(spec).__name__}"
|
|
70
|
+
)
|
|
71
|
+
self._tools.append(spec)
|
|
72
|
+
name = spec.get("name")
|
|
73
|
+
callable_target = getattr(tool, "__sectum_callable__", tool)
|
|
74
|
+
if isinstance(name, str) and name and callable(callable_target):
|
|
75
|
+
self._tool_targets[name] = callable_target
|
|
76
|
+
|
|
77
|
+
def run_turn(self, messages: list[dict[str, Any]]) -> tuple[str, tuple[str, ...]]:
|
|
78
|
+
"""Drive the tool-use loop to completion; return (final_text, tool_names)."""
|
|
79
|
+
tool_names: list[str] = []
|
|
80
|
+
loop_messages = list(messages)
|
|
81
|
+
for _ in range(_MAX_TOOL_USE_ITERATIONS):
|
|
82
|
+
response = self._anthropic.messages.create(
|
|
83
|
+
model=self._model,
|
|
84
|
+
max_tokens=self._max_tokens,
|
|
85
|
+
system=self._system,
|
|
86
|
+
tools=self._tools,
|
|
87
|
+
messages=loop_messages,
|
|
88
|
+
)
|
|
89
|
+
content_blocks = getattr(response, "content", []) or []
|
|
90
|
+
tool_uses = [
|
|
91
|
+
block for block in content_blocks if getattr(block, "type", None) == "tool_use"
|
|
92
|
+
]
|
|
93
|
+
if not tool_uses:
|
|
94
|
+
# No more tool calls; collect the final assistant text and stop.
|
|
95
|
+
final_text = _collect_text(content_blocks)
|
|
96
|
+
return final_text, tuple(tool_names)
|
|
97
|
+
|
|
98
|
+
# Append the assistant's tool_use turn to the loop history so the
|
|
99
|
+
# next messages.create call has the context.
|
|
100
|
+
loop_messages.append(
|
|
101
|
+
{
|
|
102
|
+
"role": "assistant",
|
|
103
|
+
"content": [_block_to_dict(block) for block in content_blocks],
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Execute each tool_use block and build the matching
|
|
108
|
+
# tool_result user message.
|
|
109
|
+
tool_results: list[dict[str, Any]] = []
|
|
110
|
+
for block in tool_uses:
|
|
111
|
+
name = getattr(block, "name", None)
|
|
112
|
+
if not isinstance(name, str) or not name:
|
|
113
|
+
continue
|
|
114
|
+
tool_names.append(name)
|
|
115
|
+
tool_use_id = getattr(block, "id", "")
|
|
116
|
+
arguments = getattr(block, "input", None)
|
|
117
|
+
if not isinstance(arguments, dict):
|
|
118
|
+
arguments = {}
|
|
119
|
+
target = self._tool_targets.get(name)
|
|
120
|
+
if target is None:
|
|
121
|
+
result = ""
|
|
122
|
+
else:
|
|
123
|
+
try:
|
|
124
|
+
result = target(**arguments)
|
|
125
|
+
except Exception as error:
|
|
126
|
+
result = f"tool {name} raised {type(error).__name__}: {error}"
|
|
127
|
+
tool_results.append(
|
|
128
|
+
{
|
|
129
|
+
"type": "tool_result",
|
|
130
|
+
"tool_use_id": tool_use_id,
|
|
131
|
+
"content": str(result),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
loop_messages.append({"role": "user", "content": tool_results})
|
|
135
|
+
|
|
136
|
+
raise AdapterError(
|
|
137
|
+
f"anthropic tool-use loop exceeded {_MAX_TOOL_USE_ITERATIONS} iterations"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _collect_text(content_blocks: list[Any]) -> str:
|
|
142
|
+
"""Concatenate every ``text`` block's text into a single string."""
|
|
143
|
+
parts: list[str] = []
|
|
144
|
+
for block in content_blocks:
|
|
145
|
+
if getattr(block, "type", None) != "text":
|
|
146
|
+
continue
|
|
147
|
+
text = getattr(block, "text", "")
|
|
148
|
+
if isinstance(text, str):
|
|
149
|
+
parts.append(text)
|
|
150
|
+
return "".join(parts)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _block_to_dict(block: Any) -> dict[str, Any]:
|
|
154
|
+
"""Convert an SDK content-block object back to its dict form.
|
|
155
|
+
|
|
156
|
+
Anthropic's SDK returns typed block objects on responses but expects
|
|
157
|
+
dict-shaped blocks when echoing them back as assistant history; the
|
|
158
|
+
serialisation has to round-trip without losing the type tag.
|
|
159
|
+
"""
|
|
160
|
+
block_type = getattr(block, "type", None)
|
|
161
|
+
if block_type == "text":
|
|
162
|
+
return {"type": "text", "text": getattr(block, "text", "")}
|
|
163
|
+
if block_type == "tool_use":
|
|
164
|
+
input_value = getattr(block, "input", None)
|
|
165
|
+
if isinstance(input_value, str):
|
|
166
|
+
try:
|
|
167
|
+
input_value = json.loads(input_value)
|
|
168
|
+
except json.JSONDecodeError:
|
|
169
|
+
input_value = {}
|
|
170
|
+
if not isinstance(input_value, dict):
|
|
171
|
+
input_value = {}
|
|
172
|
+
return {
|
|
173
|
+
"type": "tool_use",
|
|
174
|
+
"id": getattr(block, "id", ""),
|
|
175
|
+
"name": getattr(block, "name", ""),
|
|
176
|
+
"input": input_value,
|
|
177
|
+
}
|
|
178
|
+
# Unknown block type; fall back to a string projection so the loop
|
|
179
|
+
# does not crash on a future SDK addition.
|
|
180
|
+
return {"type": block_type or "unknown", "text": str(block)}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Live OpenAI Assistants client implementation.
|
|
2
|
+
|
|
3
|
+
Imported lazily by ``OpenAIAssistantsAgent.connect``. The module imports
|
|
4
|
+
``openai`` at module load, so a downstream import of
|
|
5
|
+
``sectum_ai.adapters.agent.openai_assistants`` does NOT pull the SDK —
|
|
6
|
+
only construction via ``connect`` does.
|
|
7
|
+
|
|
8
|
+
Implements the ``_AssistantsClient`` protocol the adapter consumes:
|
|
9
|
+
|
|
10
|
+
- ``create_assistant`` — creates the persistent Assistant (model +
|
|
11
|
+
instructions + tools); returns its id
|
|
12
|
+
- ``create_thread`` — creates a fresh conversation Thread; returns its
|
|
13
|
+
id (the adapter caches one per tenant)
|
|
14
|
+
- ``add_user_message`` — posts a user message into the Thread
|
|
15
|
+
- ``run_until_complete`` — starts a Run and drives it through the
|
|
16
|
+
tool-call resolution loop until ``completed`` (or a terminal
|
|
17
|
+
failure); returns ``(final_assistant_text, tool_call_names)``
|
|
18
|
+
|
|
19
|
+
Tool execution: the live backend resolves each ``requires_action``
|
|
20
|
+
event by reading the tool name + arguments off the run, calling the
|
|
21
|
+
caller-supplied Python callable mapped under the same name, and
|
|
22
|
+
posting the result back via ``submit_tool_outputs``. The caller
|
|
23
|
+
attaches a python callable to each tool spec via the
|
|
24
|
+
``__sectum_callable__`` attribute so the backend can find it.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from sectum_ai.adapters.agent.openai_assistants import _poll_delay_ms
|
|
34
|
+
from sectum_ai.spec import AdapterError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LiveAssistantsClient:
|
|
38
|
+
"""Live ``_AssistantsClient`` implementation backed by the OpenAI Python SDK.
|
|
39
|
+
|
|
40
|
+
Holds a single ``openai.OpenAI`` client and a registry of the
|
|
41
|
+
Python callables that back each tool name on the configured
|
|
42
|
+
Assistant.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
46
|
+
try:
|
|
47
|
+
from openai import OpenAI
|
|
48
|
+
except ImportError as error:
|
|
49
|
+
raise AdapterError(
|
|
50
|
+
"openai-assistants adapter requires the `openai` package; "
|
|
51
|
+
"install sectum-ai-adapters[openai-assistants] to enable it"
|
|
52
|
+
) from error
|
|
53
|
+
self._openai = OpenAI(api_key=api_key) if api_key else OpenAI()
|
|
54
|
+
self._tools: dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
def create_assistant(
|
|
57
|
+
self,
|
|
58
|
+
model: str,
|
|
59
|
+
tools: list[Any],
|
|
60
|
+
*,
|
|
61
|
+
name: str,
|
|
62
|
+
instructions: str,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Create a persistent Assistant + register the tool callables locally."""
|
|
65
|
+
tool_specs: list[dict[str, Any]] = []
|
|
66
|
+
for tool in tools:
|
|
67
|
+
spec = getattr(tool, "__sectum_tool_spec__", None) or tool
|
|
68
|
+
if not isinstance(spec, dict) or "function" not in spec:
|
|
69
|
+
raise AdapterError(
|
|
70
|
+
"each tool must carry a `__sectum_tool_spec__` dict with the "
|
|
71
|
+
"OpenAI function-tool schema, or be a tool-spec dict itself"
|
|
72
|
+
)
|
|
73
|
+
tool_specs.append(spec)
|
|
74
|
+
function_name = spec.get("function", {}).get("name")
|
|
75
|
+
callable_target = getattr(tool, "__sectum_callable__", tool)
|
|
76
|
+
if function_name and callable(callable_target):
|
|
77
|
+
self._tools[function_name] = callable_target
|
|
78
|
+
assistant = self._openai.beta.assistants.create(
|
|
79
|
+
model=model,
|
|
80
|
+
name=name,
|
|
81
|
+
instructions=instructions,
|
|
82
|
+
tools=tool_specs,
|
|
83
|
+
)
|
|
84
|
+
return str(assistant.id)
|
|
85
|
+
|
|
86
|
+
def create_thread(self) -> str:
|
|
87
|
+
thread = self._openai.beta.threads.create()
|
|
88
|
+
return str(thread.id)
|
|
89
|
+
|
|
90
|
+
def add_user_message(self, thread_id: str, content: str) -> None:
|
|
91
|
+
self._openai.beta.threads.messages.create(thread_id=thread_id, role="user", content=content)
|
|
92
|
+
|
|
93
|
+
def run_until_complete(self, thread_id: str, assistant_id: str) -> tuple[str, tuple[str, ...]]:
|
|
94
|
+
run = self._openai.beta.threads.runs.create(thread_id=thread_id, assistant_id=assistant_id)
|
|
95
|
+
tool_names: list[str] = []
|
|
96
|
+
attempt = 0
|
|
97
|
+
while True:
|
|
98
|
+
run = self._openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
|
|
99
|
+
status = getattr(run, "status", None)
|
|
100
|
+
if status == "completed":
|
|
101
|
+
break
|
|
102
|
+
if status in ("failed", "cancelled", "expired"):
|
|
103
|
+
raise AdapterError(
|
|
104
|
+
f"openai assistants run terminated in {status!r}: "
|
|
105
|
+
f"{getattr(run, 'last_error', None)}"
|
|
106
|
+
)
|
|
107
|
+
if status == "requires_action":
|
|
108
|
+
outputs = []
|
|
109
|
+
required = getattr(run, "required_action", None)
|
|
110
|
+
tool_calls = (
|
|
111
|
+
getattr(getattr(required, "submit_tool_outputs", None), "tool_calls", [])
|
|
112
|
+
if required is not None
|
|
113
|
+
else []
|
|
114
|
+
)
|
|
115
|
+
for call in tool_calls:
|
|
116
|
+
function = getattr(call, "function", None)
|
|
117
|
+
if function is None:
|
|
118
|
+
continue
|
|
119
|
+
name = getattr(function, "name", None)
|
|
120
|
+
if not isinstance(name, str) or not name:
|
|
121
|
+
continue
|
|
122
|
+
tool_names.append(name)
|
|
123
|
+
args_raw = getattr(function, "arguments", "") or "{}"
|
|
124
|
+
try:
|
|
125
|
+
args = json.loads(args_raw)
|
|
126
|
+
except json.JSONDecodeError:
|
|
127
|
+
args = {}
|
|
128
|
+
target = self._tools.get(name)
|
|
129
|
+
if target is None:
|
|
130
|
+
result: Any = ""
|
|
131
|
+
else:
|
|
132
|
+
result = target(**args)
|
|
133
|
+
outputs.append({"tool_call_id": getattr(call, "id", ""), "output": str(result)})
|
|
134
|
+
self._openai.beta.threads.runs.submit_tool_outputs(
|
|
135
|
+
thread_id=thread_id, run_id=run.id, tool_outputs=outputs
|
|
136
|
+
)
|
|
137
|
+
time.sleep(_poll_delay_ms(attempt) / 1000.0)
|
|
138
|
+
attempt += 1
|
|
139
|
+
|
|
140
|
+
messages = self._openai.beta.threads.messages.list(thread_id=thread_id, order="desc")
|
|
141
|
+
for message in messages.data:
|
|
142
|
+
if getattr(message, "role", None) != "assistant":
|
|
143
|
+
continue
|
|
144
|
+
content_blocks = getattr(message, "content", []) or []
|
|
145
|
+
for block in content_blocks:
|
|
146
|
+
text = getattr(getattr(block, "text", None), "value", None)
|
|
147
|
+
if isinstance(text, str) and text:
|
|
148
|
+
return text, tuple(tool_names)
|
|
149
|
+
return "", tuple(tool_names)
|