cantus-agent 0.4.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.
- cantus/__init__.py +131 -0
- cantus/adapters/__init__.py +135 -0
- cantus/adapters/_remote_skill.py +72 -0
- cantus/adapters/anthropic_memory.py +107 -0
- cantus/adapters/dspy.py +149 -0
- cantus/adapters/huggingface.py +117 -0
- cantus/adapters/langchain.py +144 -0
- cantus/adapters/mcp.py +156 -0
- cantus/adapters/mcp_client.py +164 -0
- cantus/adapters/mcp_server.py +141 -0
- cantus/adapters/openhands.py +50 -0
- cantus/config.py +73 -0
- cantus/core/__init__.py +0 -0
- cantus/core/action.py +38 -0
- cantus/core/agent.py +420 -0
- cantus/core/event_stream.py +48 -0
- cantus/core/event_stream_persistence.py +79 -0
- cantus/core/observation.py +74 -0
- cantus/core/registry.py +83 -0
- cantus/core/result.py +28 -0
- cantus/env/__init__.py +26 -0
- cantus/env/cloud_only.py +23 -0
- cantus/env/colab.py +76 -0
- cantus/env/local.py +62 -0
- cantus/grammar/__init__.py +0 -0
- cantus/grammar/tool_call.py +136 -0
- cantus/hooks/__init__.py +37 -0
- cantus/identity/__init__.py +15 -0
- cantus/identity/soul.py +157 -0
- cantus/inspect.py +41 -0
- cantus/model/__init__.py +0 -0
- cantus/model/bridge.py +46 -0
- cantus/model/chat.py +96 -0
- cantus/model/chat_template.py +62 -0
- cantus/model/factory.py +93 -0
- cantus/model/loader.py +151 -0
- cantus/model/providers/__init__.py +6 -0
- cantus/model/providers/_common.py +26 -0
- cantus/model/providers/_translate.py +339 -0
- cantus/model/providers/anthropic.py +88 -0
- cantus/model/providers/google.py +91 -0
- cantus/model/providers/groq.py +87 -0
- cantus/model/providers/nvidia.py +39 -0
- cantus/model/providers/openai.py +95 -0
- cantus/protocols/__init__.py +0 -0
- cantus/protocols/_common.py +72 -0
- cantus/protocols/analyzer.py +82 -0
- cantus/protocols/debug.py +76 -0
- cantus/protocols/memory.py +191 -0
- cantus/protocols/memory_auto.py +154 -0
- cantus/protocols/memory_markdown.py +211 -0
- cantus/protocols/skill.py +138 -0
- cantus/protocols/validator.py +114 -0
- cantus/py.typed +0 -0
- cantus/serve/__init__.py +26 -0
- cantus/serve/app.py +158 -0
- cantus/serve/channel.py +64 -0
- cantus/serve/dashboard.py +144 -0
- cantus/serve/security.py +144 -0
- cantus/workflows/__init__.py +24 -0
- cantus/workflows/evaluator_optimizer.py +49 -0
- cantus/workflows/orchestrator_worker.py +41 -0
- cantus/workflows/parallel.py +24 -0
- cantus/workflows/prompt_chain.py +26 -0
- cantus/workflows/router.py +36 -0
- cantus_agent-0.4.2.dist-info/METADATA +281 -0
- cantus_agent-0.4.2.dist-info/RECORD +70 -0
- cantus_agent-0.4.2.dist-info/WHEEL +5 -0
- cantus_agent-0.4.2.dist-info/licenses/LICENSE +203 -0
- cantus_agent-0.4.2.dist-info/top_level.txt +1 -0
cantus/__init__.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""cantus — Polyphonic LLM agent framework with a dual-tier teaching API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from cantus.model.loader import ModelHandle
|
|
9
|
+
|
|
10
|
+
__version__ = "0.4.2"
|
|
11
|
+
|
|
12
|
+
from cantus.core.action import (
|
|
13
|
+
Action,
|
|
14
|
+
CallSkillAction,
|
|
15
|
+
FinalAnswerAction,
|
|
16
|
+
)
|
|
17
|
+
from cantus.core.agent import Agent, AgentState
|
|
18
|
+
from cantus.core.event_stream import EventStream
|
|
19
|
+
from cantus.core.event_stream_persistence import JsonLinesPersistence
|
|
20
|
+
from cantus.core.observation import (
|
|
21
|
+
MaxIterationsObservation,
|
|
22
|
+
Observation,
|
|
23
|
+
SkillObservation,
|
|
24
|
+
ToolErrorObservation,
|
|
25
|
+
ValidationErrorObservation,
|
|
26
|
+
)
|
|
27
|
+
from cantus.core.registry import Registry, get_registry
|
|
28
|
+
from cantus.core.result import Result
|
|
29
|
+
from cantus.env import CloudOnlyEnvironment, ColabEnvironment, LocalEnvironment
|
|
30
|
+
from cantus.identity import Soul, SoulParseError
|
|
31
|
+
from cantus.inspect import Inspector
|
|
32
|
+
from cantus.model.bridge import ChatModelAsHandle
|
|
33
|
+
from cantus.model.chat import ChatModel, ChatResponse, Message, ToolCall
|
|
34
|
+
from cantus.model.factory import load_chat_model
|
|
35
|
+
from cantus.protocols.debug import debug
|
|
36
|
+
from cantus.protocols.memory import (
|
|
37
|
+
AutoMemory,
|
|
38
|
+
BM25Memory,
|
|
39
|
+
EmbeddingMemory,
|
|
40
|
+
MarkdownMemory,
|
|
41
|
+
Memory,
|
|
42
|
+
ShortTermMemory,
|
|
43
|
+
)
|
|
44
|
+
from cantus.protocols.skill import Skill, register_skill, skill
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Decorator entries (v0.3.0: Skill + debug only at top level;
|
|
48
|
+
# analyzer/validator are now imported from cantus.hooks)
|
|
49
|
+
"skill",
|
|
50
|
+
"debug",
|
|
51
|
+
# Function-pass entries
|
|
52
|
+
"register_skill",
|
|
53
|
+
# Class-first base classes (top level: Skill + Memory only;
|
|
54
|
+
# Analyzer/Validator are imported from cantus.hooks)
|
|
55
|
+
"Skill",
|
|
56
|
+
"Memory",
|
|
57
|
+
# Memory implementations (v0.3.1: MarkdownMemory + AutoMemory join)
|
|
58
|
+
"ShortTermMemory",
|
|
59
|
+
"BM25Memory",
|
|
60
|
+
"EmbeddingMemory",
|
|
61
|
+
"MarkdownMemory",
|
|
62
|
+
"AutoMemory",
|
|
63
|
+
# Identity (v0.3.1)
|
|
64
|
+
"Soul",
|
|
65
|
+
"SoulParseError",
|
|
66
|
+
# EventStream persistence plug (v0.3.1)
|
|
67
|
+
"JsonLinesPersistence",
|
|
68
|
+
# Runtime
|
|
69
|
+
"Action",
|
|
70
|
+
"CallSkillAction",
|
|
71
|
+
"FinalAnswerAction",
|
|
72
|
+
"Observation",
|
|
73
|
+
"SkillObservation",
|
|
74
|
+
"ToolErrorObservation",
|
|
75
|
+
"ValidationErrorObservation",
|
|
76
|
+
"MaxIterationsObservation",
|
|
77
|
+
"EventStream",
|
|
78
|
+
"Agent",
|
|
79
|
+
"AgentState",
|
|
80
|
+
"Inspector",
|
|
81
|
+
# Registry
|
|
82
|
+
"Registry",
|
|
83
|
+
"get_registry",
|
|
84
|
+
# Result type
|
|
85
|
+
"Result",
|
|
86
|
+
# Tier 2 ChatModel
|
|
87
|
+
"ChatModel",
|
|
88
|
+
"Message",
|
|
89
|
+
"ToolCall",
|
|
90
|
+
"ChatResponse",
|
|
91
|
+
"ChatModelAsHandle",
|
|
92
|
+
"load_chat_model",
|
|
93
|
+
# Environment profiles
|
|
94
|
+
"ColabEnvironment",
|
|
95
|
+
"LocalEnvironment",
|
|
96
|
+
"CloudOnlyEnvironment",
|
|
97
|
+
# Serve layer (v0.4.0, lazy-loaded; require `pip install cantus[serve]`)
|
|
98
|
+
"serve",
|
|
99
|
+
"config",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def __getattr__(name: str) -> Any:
|
|
104
|
+
"""PEP 562 lazy loader for the v0.4.0 serve layer.
|
|
105
|
+
|
|
106
|
+
`cantus.serve` and `cantus.config` are exposed at the top level but
|
|
107
|
+
imported on first attribute access so that a base ``import cantus``
|
|
108
|
+
does not require ``fastapi`` / ``uvicorn`` / ``pydantic-settings``.
|
|
109
|
+
Missing extras still surface as ``ImportError`` with the literal
|
|
110
|
+
substring ``"pip install cantus[serve]"`` from the gate inside the
|
|
111
|
+
target module.
|
|
112
|
+
"""
|
|
113
|
+
if name in {"serve", "config"}:
|
|
114
|
+
import importlib
|
|
115
|
+
|
|
116
|
+
module = importlib.import_module(f"cantus.{name}")
|
|
117
|
+
globals()[name] = module
|
|
118
|
+
return module
|
|
119
|
+
raise AttributeError(f"module 'cantus' has no attribute {name!r}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mount_drive_and_load(*args: object, **kwargs: object) -> ModelHandle:
|
|
123
|
+
"""Mount Google Drive and load Gemma 4 weights — see model.loader."""
|
|
124
|
+
from cantus.model.loader import mount_drive_and_load as _impl
|
|
125
|
+
|
|
126
|
+
return _impl(*args, **kwargs) # type: ignore[arg-type]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_gemma(*args: object, **kwargs: object) -> ModelHandle:
|
|
130
|
+
"""Alias of mount_drive_and_load for direct hub loading scenarios."""
|
|
131
|
+
return mount_drive_and_load(*args, **kwargs)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""cantus.adapters — bridges to MCP, Anthropic Memory, and four
|
|
2
|
+
cross-framework targets (LangChain / DSPy / HuggingFace / OpenHands).
|
|
3
|
+
|
|
4
|
+
The adapter layer is a pure conversion utility: it does NOT alter Skill
|
|
5
|
+
or Memory runtime behaviour and does NOT register a new protocol kind.
|
|
6
|
+
The package exposes ten top-level callables (3 from v0.3.2 + 6 from
|
|
7
|
+
v0.3.3 + 1 from v0.3.4):
|
|
8
|
+
|
|
9
|
+
v0.3.2 (MCP + Anthropic Memory):
|
|
10
|
+
- ``export_as_mcp_server(skills, *, name, version)`` — wrap a list of
|
|
11
|
+
cantus Skills as an MCP server (requires ``cantus[mcp]``).
|
|
12
|
+
- ``import_mcp_server(*, transport, command_or_url)`` — connect to a
|
|
13
|
+
remote MCP server and return a list of cantus Skills.
|
|
14
|
+
- ``expose_as_anthropic_memory_tool(memory)`` — JSON-serialisable dict
|
|
15
|
+
matching the Anthropic Memory tool spec (no SDK dependency).
|
|
16
|
+
|
|
17
|
+
v0.3.3 (cross-framework batch2):
|
|
18
|
+
- ``expose_as_langchain_tool(skill)`` / ``import_langchain_tool(tool)`` —
|
|
19
|
+
cantus Skill <-> ``langchain_core.tools.BaseTool``
|
|
20
|
+
(requires ``cantus[langchain]``).
|
|
21
|
+
- ``expose_as_dspy_tool(skill)`` / ``import_dspy_tool(tool)`` —
|
|
22
|
+
cantus Skill <-> ``dspy.Tool``
|
|
23
|
+
(requires ``cantus[dspy]``).
|
|
24
|
+
- ``expose_as_hf_tool(skill)`` — cantus Skill -> ``transformers.Tool``
|
|
25
|
+
(requires ``cantus[huggingface]``).
|
|
26
|
+
- ``expose_as_openhands_action(skill)`` — cantus Skill ->
|
|
27
|
+
``openhands.events.Action`` (requires ``cantus[openhands]``;
|
|
28
|
+
export-only — the import direction is permanently not applicable
|
|
29
|
+
because ``Action`` is a declarative event record dispatched by the
|
|
30
|
+
OpenHands host runtime, with no ``__call__`` for ``Skill.run`` to
|
|
31
|
+
delegate to).
|
|
32
|
+
|
|
33
|
+
v0.3.4 (cross-framework batch3a — HF bidirectional close-out):
|
|
34
|
+
- ``import_hf_tool(tool)`` — cantus Skill <- ``transformers.Tool``
|
|
35
|
+
(requires ``cantus[huggingface]``). Completes the HuggingFace
|
|
36
|
+
bidirectional matrix; the OpenHands import direction is intentionally
|
|
37
|
+
omitted, see ``expose_as_openhands_action`` above.
|
|
38
|
+
|
|
39
|
+
Every batch2/batch3 entry lazy-imports its SDK only when called; the core
|
|
40
|
+
``pip install cantus`` install still resolves ``import cantus.adapters``
|
|
41
|
+
without pulling any framework SDK.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def export_as_mcp_server(*args: Any, **kwargs: Any) -> Any:
|
|
50
|
+
"""Stub. Implemented in cantus.adapters.mcp_server."""
|
|
51
|
+
from cantus.adapters.mcp_server import export_as_mcp_server as _impl
|
|
52
|
+
|
|
53
|
+
return _impl(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def import_mcp_server(*args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
"""Stub. Implemented in cantus.adapters.mcp_client."""
|
|
58
|
+
from cantus.adapters.mcp_client import import_mcp_server as _impl
|
|
59
|
+
|
|
60
|
+
return _impl(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def expose_as_anthropic_memory_tool(*args: Any, **kwargs: Any) -> Any:
|
|
64
|
+
"""Stub. Implemented in cantus.adapters.anthropic_memory."""
|
|
65
|
+
from cantus.adapters.anthropic_memory import (
|
|
66
|
+
expose_as_anthropic_memory_tool as _impl,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return _impl(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def expose_as_langchain_tool(*args: Any, **kwargs: Any) -> Any:
|
|
73
|
+
"""Stub. Implemented in cantus.adapters.langchain (requires cantus[langchain])."""
|
|
74
|
+
from cantus.adapters.langchain import expose_as_langchain_tool as _impl
|
|
75
|
+
|
|
76
|
+
return _impl(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def import_langchain_tool(*args: Any, **kwargs: Any) -> Any:
|
|
80
|
+
"""Stub. Implemented in cantus.adapters.langchain (requires cantus[langchain])."""
|
|
81
|
+
from cantus.adapters.langchain import import_langchain_tool as _impl
|
|
82
|
+
|
|
83
|
+
return _impl(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def expose_as_dspy_tool(*args: Any, **kwargs: Any) -> Any:
|
|
87
|
+
"""Stub. Implemented in cantus.adapters.dspy (requires cantus[dspy])."""
|
|
88
|
+
from cantus.adapters.dspy import expose_as_dspy_tool as _impl
|
|
89
|
+
|
|
90
|
+
return _impl(*args, **kwargs)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def import_dspy_tool(*args: Any, **kwargs: Any) -> Any:
|
|
94
|
+
"""Stub. Implemented in cantus.adapters.dspy (requires cantus[dspy])."""
|
|
95
|
+
from cantus.adapters.dspy import import_dspy_tool as _impl
|
|
96
|
+
|
|
97
|
+
return _impl(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def expose_as_hf_tool(*args: Any, **kwargs: Any) -> Any:
|
|
101
|
+
"""Stub. Implemented in cantus.adapters.huggingface (requires cantus[huggingface])."""
|
|
102
|
+
from cantus.adapters.huggingface import expose_as_hf_tool as _impl
|
|
103
|
+
|
|
104
|
+
return _impl(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def import_hf_tool(*args: Any, **kwargs: Any) -> Any:
|
|
108
|
+
"""Stub. Implemented in cantus.adapters.huggingface (requires cantus[huggingface])."""
|
|
109
|
+
from cantus.adapters.huggingface import import_hf_tool as _impl
|
|
110
|
+
|
|
111
|
+
return _impl(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def expose_as_openhands_action(*args: Any, **kwargs: Any) -> Any:
|
|
115
|
+
"""Stub. Implemented in cantus.adapters.openhands (requires cantus[openhands])."""
|
|
116
|
+
from cantus.adapters.openhands import expose_as_openhands_action as _impl
|
|
117
|
+
|
|
118
|
+
return _impl(*args, **kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
# v0.3.2 — MCP + Anthropic Memory
|
|
123
|
+
"export_as_mcp_server",
|
|
124
|
+
"import_mcp_server",
|
|
125
|
+
"expose_as_anthropic_memory_tool",
|
|
126
|
+
# v0.3.3 — cross-framework batch2
|
|
127
|
+
"expose_as_langchain_tool",
|
|
128
|
+
"import_langchain_tool",
|
|
129
|
+
"expose_as_dspy_tool",
|
|
130
|
+
"import_dspy_tool",
|
|
131
|
+
"expose_as_hf_tool",
|
|
132
|
+
"expose_as_openhands_action",
|
|
133
|
+
# v0.3.4 — cross-framework batch3a (HF bidirectional close-out)
|
|
134
|
+
"import_hf_tool",
|
|
135
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Shared base class for all `cantus.adapters.import_*` adapters.
|
|
2
|
+
|
|
3
|
+
`_RemoteSkillBase` lifts the three load-bearing patterns from v0.3.2
|
|
4
|
+
`cantus.adapters.mcp_client._RemoteSkill` into a private, framework-internal
|
|
5
|
+
base so that LangChain / DSPy / future cross-framework import adapters
|
|
6
|
+
inherit the same v0.3.0 `Skill.spec_for_llm()` shape contract instead of
|
|
7
|
+
duplicating it per adapter:
|
|
8
|
+
|
|
9
|
+
1. ``__init__`` bypasses :class:`Skill`'s signature-introspection path —
|
|
10
|
+
the remote framework's schema is authoritative.
|
|
11
|
+
2. ``spec_for_llm()`` returns the canonical three-key dict directly from
|
|
12
|
+
the remote schema dict; ``is_remote = True`` is NOT leaked into it.
|
|
13
|
+
3. ``validate_args()`` trusts the remote framework's schema and only
|
|
14
|
+
enforces dict shape at the protocol layer.
|
|
15
|
+
|
|
16
|
+
This module is private (leading underscore in module name) and SHALL NOT
|
|
17
|
+
be re-exported from ``cantus.adapters.__init__``; the adapter-layer-batch2
|
|
18
|
+
spec explicitly forbids exposing an ``Adapter`` ABC as public surface.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from cantus.protocols.skill import Skill
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _RemoteSkillBase(Skill):
|
|
29
|
+
"""Internal base for cantus Skills that proxy a remote framework tool."""
|
|
30
|
+
|
|
31
|
+
is_remote = True
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
name: str,
|
|
37
|
+
description: str,
|
|
38
|
+
args_schema_dict: dict[str, Any],
|
|
39
|
+
) -> None:
|
|
40
|
+
# Intentionally bypass Skill.__init__: remote frameworks (MCP,
|
|
41
|
+
# LangChain, DSPy, OpenHands) supply their own authoritative
|
|
42
|
+
# schema, so signature introspection of `run` is both useless
|
|
43
|
+
# (the body just dispatches kwargs) and wrong (it would invent
|
|
44
|
+
# an empty Pydantic model with no fields).
|
|
45
|
+
self.name = name
|
|
46
|
+
self.description = description
|
|
47
|
+
self._args_schema_dict = args_schema_dict
|
|
48
|
+
self._pre_hook = None
|
|
49
|
+
self._post_hook = None
|
|
50
|
+
|
|
51
|
+
def spec_for_llm(self) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"description": self.description,
|
|
55
|
+
"args_schema": self._args_schema_dict,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def validate_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
if not isinstance(args, dict):
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"remote adapter tool args must be a dict, "
|
|
62
|
+
f"got {type(args).__name__}"
|
|
63
|
+
)
|
|
64
|
+
return dict(args)
|
|
65
|
+
|
|
66
|
+
def run(self, **kwargs: Any) -> Any:
|
|
67
|
+
raise NotImplementedError(
|
|
68
|
+
"subclass must implement run() with framework-specific dispatch"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["_RemoteSkillBase"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""expose_as_anthropic_memory_tool — cantus Memory → Anthropic Memory tool dict.
|
|
2
|
+
|
|
3
|
+
Returns a pure Python `dict` (no callables, no Memory instance references)
|
|
4
|
+
matching the Anthropic Memory tool spec: a 4-action surface (`view`,
|
|
5
|
+
`create`, `str_replace`, `delete`) over a Skill-shaped command schema. The
|
|
6
|
+
returned dict round-trips through `json.dumps` so host code can feed it
|
|
7
|
+
straight into `client.messages.create(tools=[...])`.
|
|
8
|
+
|
|
9
|
+
The adapter does NOT wire the LLM's tool_use callbacks back into the
|
|
10
|
+
Memory backend — that's host code's responsibility. The returned dict is
|
|
11
|
+
opaque to the Memory instance; the host pattern is:
|
|
12
|
+
|
|
13
|
+
1. send `tool_dict` to Anthropic via `tools=[...]`
|
|
14
|
+
2. when Claude emits `tool_use` with `name="memory"`, dispatch the
|
|
15
|
+
`command` argument back into the appropriate `memory.recall` /
|
|
16
|
+
`memory.remember` call
|
|
17
|
+
|
|
18
|
+
Foot-gun warning carry-over (v0.3.1 audit Trap-10): under this tool_use
|
|
19
|
+
loop, the LLM has full CRUD access to the memory backend with no built-in
|
|
20
|
+
filter. For production, wrap individual commands with validation in the
|
|
21
|
+
host dispatch layer.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from cantus.protocols.memory import AutoMemory, Memory
|
|
29
|
+
|
|
30
|
+
_VIEW_SCHEMA: dict[str, Any] = {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"query": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Substring to match against stored turn content (case-insensitive).",
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["query"],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_CREATE_SCHEMA: dict[str, Any] = {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"user": {"type": "string", "description": "User-side content of the turn."},
|
|
45
|
+
"assistant": {"type": "string", "description": "Assistant-side content of the turn."},
|
|
46
|
+
},
|
|
47
|
+
"required": ["user", "assistant"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_STR_REPLACE_SCHEMA: dict[str, Any] = {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"query": {"type": "string", "description": "Substring filter selecting turns to modify."},
|
|
54
|
+
"old": {"type": "string", "description": "Old substring to replace within matched turns."},
|
|
55
|
+
"new": {"type": "string", "description": "Replacement substring."},
|
|
56
|
+
},
|
|
57
|
+
"required": ["query", "old", "new"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_DELETE_SCHEMA: dict[str, Any] = {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"query": {"type": "string", "description": "Substring filter selecting turns to delete."},
|
|
64
|
+
},
|
|
65
|
+
"required": ["query"],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def expose_as_anthropic_memory_tool(memory: Memory | AutoMemory) -> dict[str, Any]:
|
|
70
|
+
"""Return a JSON-serialisable Anthropic Memory tool dict for the given Memory."""
|
|
71
|
+
if not isinstance(memory, (Memory, AutoMemory)):
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"expose_as_anthropic_memory_tool expects Memory or AutoMemory, "
|
|
74
|
+
f"got {type(memory).__name__}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
backend_name = type(memory).__name__
|
|
78
|
+
description = f"Cantus {backend_name} exposed as Anthropic Memory tool"
|
|
79
|
+
|
|
80
|
+
commands: dict[str, dict[str, Any]] = {
|
|
81
|
+
"view": {
|
|
82
|
+
"description": "Recall stored turns whose user or assistant field matches the query.",
|
|
83
|
+
"args_schema": _VIEW_SCHEMA,
|
|
84
|
+
},
|
|
85
|
+
"create": {
|
|
86
|
+
"description": "Remember a new (user, assistant) turn.",
|
|
87
|
+
"args_schema": _CREATE_SCHEMA,
|
|
88
|
+
},
|
|
89
|
+
"str_replace": {
|
|
90
|
+
"description": "Find matching turns and replace `old` with `new` in their fields.",
|
|
91
|
+
"args_schema": _STR_REPLACE_SCHEMA,
|
|
92
|
+
},
|
|
93
|
+
"delete": {
|
|
94
|
+
"description": "Mark stored turns matching the query as deleted (backend-dependent).",
|
|
95
|
+
"args_schema": _DELETE_SCHEMA,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"type": "memory",
|
|
101
|
+
"name": "memory",
|
|
102
|
+
"description": description,
|
|
103
|
+
"commands": commands,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["expose_as_anthropic_memory_tool"]
|
cantus/adapters/dspy.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""DSPy adapter — bidirectional cantus <-> dspy.Tool.
|
|
2
|
+
|
|
3
|
+
Schema conversion follows the v0.3.3 design.md "LangChain / DSPy import_*
|
|
4
|
+
schema 轉換策略" decision: types are mapped via a fixed JSON Schema
|
|
5
|
+
``{str, int, float, bool}`` <-> Python type table, with other types
|
|
6
|
+
falling back to ``str`` / ``"string"``. The framework does NOT attempt
|
|
7
|
+
universal coverage of complex generics (``list[str]``, ``Optional[X]``,
|
|
8
|
+
unions); that complexity is intentionally pushed to a future revision
|
|
9
|
+
when real student demand surfaces.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
try: # SDK gate — fail loud the moment this module is imported.
|
|
17
|
+
import dspy
|
|
18
|
+
except ImportError as exc:
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"cantus.adapters.dspy requires the dspy-ai SDK. "
|
|
21
|
+
"Run: pip install cantus[dspy]"
|
|
22
|
+
) from exc
|
|
23
|
+
|
|
24
|
+
from cantus.adapters._remote_skill import _RemoteSkillBase
|
|
25
|
+
from cantus.protocols.skill import Skill
|
|
26
|
+
|
|
27
|
+
_JSON_TYPE_TO_PY: dict[str, type] = {
|
|
28
|
+
"string": str,
|
|
29
|
+
"integer": int,
|
|
30
|
+
"number": float,
|
|
31
|
+
"boolean": bool,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_PY_TYPE_TO_JSON: dict[type, str] = {
|
|
35
|
+
str: "string",
|
|
36
|
+
int: "integer",
|
|
37
|
+
float: "number",
|
|
38
|
+
bool: "boolean",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _py_type_to_json_type(py_type: Any) -> str:
|
|
43
|
+
return _PY_TYPE_TO_JSON.get(py_type, "string")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _CantusDspyInputField:
|
|
47
|
+
"""Shape that mirrors a dspy input-field descriptor for exposed Skills."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, py_type: type, *, optional: bool = False) -> None:
|
|
50
|
+
self.py_type = py_type
|
|
51
|
+
self.optional = optional
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _CantusDspySignature:
|
|
55
|
+
"""Minimal signature shape with an ``input_fields`` dict."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, input_fields: dict[str, _CantusDspyInputField]) -> None:
|
|
58
|
+
self.input_fields = input_fields
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def expose_as_dspy_tool(skill: Skill) -> "dspy.Tool":
|
|
62
|
+
"""Wrap a cantus Skill as a dspy.Tool instance."""
|
|
63
|
+
if not isinstance(skill, Skill):
|
|
64
|
+
raise TypeError(
|
|
65
|
+
f"expose_as_dspy_tool expects Skill, got {type(skill).__name__}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
spec = skill.spec_for_llm()
|
|
69
|
+
properties = (spec["args_schema"].get("properties") or {})
|
|
70
|
+
required = set(spec["args_schema"].get("required") or [])
|
|
71
|
+
|
|
72
|
+
input_fields: dict[str, Any] = {}
|
|
73
|
+
for prop_name, prop_schema in properties.items():
|
|
74
|
+
json_type = prop_schema.get("type", "string")
|
|
75
|
+
py_type = _JSON_TYPE_TO_PY.get(json_type, str)
|
|
76
|
+
optional = prop_name not in required
|
|
77
|
+
input_fields[prop_name] = _CantusDspyInputField(py_type, optional=optional)
|
|
78
|
+
|
|
79
|
+
signature = _CantusDspySignature(input_fields)
|
|
80
|
+
|
|
81
|
+
def _impl(**kwargs: Any) -> Any:
|
|
82
|
+
return skill(**kwargs)
|
|
83
|
+
|
|
84
|
+
return dspy.Tool(
|
|
85
|
+
name=spec["name"],
|
|
86
|
+
desc=spec["description"],
|
|
87
|
+
signature=signature,
|
|
88
|
+
impl=_impl,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _DspyRemoteSkill(_RemoteSkillBase):
|
|
93
|
+
"""cantus Skill that proxies a dspy.Tool."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, *, tool: "dspy.Tool") -> None:
|
|
96
|
+
signature = getattr(tool, "signature", None)
|
|
97
|
+
try:
|
|
98
|
+
input_fields = signature.input_fields # type: ignore[union-attr]
|
|
99
|
+
if not isinstance(input_fields, dict):
|
|
100
|
+
raise TypeError(
|
|
101
|
+
f"input_fields must be a dict, got {type(input_fields).__name__}"
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"dspy_handshake_failed: cannot read signature.input_fields: "
|
|
106
|
+
f"{type(exc).__name__}: {exc}"
|
|
107
|
+
) from exc
|
|
108
|
+
|
|
109
|
+
properties: dict[str, dict[str, Any]] = {}
|
|
110
|
+
required: list[str] = []
|
|
111
|
+
for field_name, field in input_fields.items():
|
|
112
|
+
py_type = getattr(field, "py_type", str)
|
|
113
|
+
properties[field_name] = {"type": _py_type_to_json_type(py_type)}
|
|
114
|
+
if not getattr(field, "optional", False):
|
|
115
|
+
required.append(field_name)
|
|
116
|
+
|
|
117
|
+
args_schema_dict: dict[str, Any] = {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": properties,
|
|
120
|
+
"required": required,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
super().__init__(
|
|
124
|
+
name=tool.name,
|
|
125
|
+
description=getattr(tool, "desc", "") or "",
|
|
126
|
+
args_schema_dict=args_schema_dict,
|
|
127
|
+
)
|
|
128
|
+
self._tool = tool
|
|
129
|
+
|
|
130
|
+
def run(self, **kwargs: Any) -> Any:
|
|
131
|
+
try:
|
|
132
|
+
return self._tool(**kwargs)
|
|
133
|
+
except Exception as exc: # noqa: BLE001
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
f"dspy_remote_error: tool {self.name!r} failed: "
|
|
136
|
+
f"{type(exc).__name__}: {exc}"
|
|
137
|
+
) from exc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def import_dspy_tool(tool: "dspy.Tool") -> Skill:
|
|
141
|
+
"""Wrap a dspy.Tool as a cantus Skill instance."""
|
|
142
|
+
if not isinstance(tool, dspy.Tool):
|
|
143
|
+
raise TypeError(
|
|
144
|
+
f"import_dspy_tool expects dspy.Tool, got {type(tool).__name__}"
|
|
145
|
+
)
|
|
146
|
+
return _DspyRemoteSkill(tool=tool)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["expose_as_dspy_tool", "import_dspy_tool"]
|