chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"""Framework adapters for lean-ctx tools (EPIC 12.6).
|
||||
|
||||
Thin, optional integrations that expose the lean-ctx tool surface to popular
|
||||
agent frameworks. Each framework is an *optional* dependency, imported lazily —
|
||||
installing the SDK pulls in none of them. The OpenAI adapter is a pure
|
||||
transformation and needs no extra package.
|
||||
|
||||
from leanctx import LeanCtxClient
|
||||
from leanctx.adapters import to_openai_tools, to_langchain_tools
|
||||
|
||||
client = LeanCtxClient("http://127.0.0.1:8080")
|
||||
tools = to_openai_tools(client)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._common import ToolSpec, coerce_arguments, normalized_tool_specs
|
||||
from .crewai import to_crewai_tools
|
||||
from .langchain import to_langchain_tools
|
||||
from .llamaindex import to_llamaindex_tools
|
||||
from .openai import run_openai_tool_call, to_openai_tools
|
||||
|
||||
__all__ = [
|
||||
"ToolSpec",
|
||||
"normalized_tool_specs",
|
||||
"coerce_arguments",
|
||||
"to_openai_tools",
|
||||
"run_openai_tool_call",
|
||||
"to_langchain_tools",
|
||||
"to_llamaindex_tools",
|
||||
"to_crewai_tools",
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Shared helpers for framework adapters (EPIC 12.6).
|
||||
|
||||
All adapters normalize lean-ctx tools into a common [`ToolSpec`] and reuse the
|
||||
same SDK call path (`call_tool_text`), so every framework integration behaves
|
||||
identically and stays correct as the tool surface evolves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
from ..client import LeanCtxClient
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolSpec:
|
||||
"""A framework-neutral description of one lean-ctx tool."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
parameters: Dict[str, Any] # JSON Schema for the arguments object
|
||||
|
||||
|
||||
def _default_schema() -> Dict[str, Any]:
|
||||
return {"type": "object", "properties": {}}
|
||||
|
||||
|
||||
def normalized_tool_specs(client: LeanCtxClient, *, limit: int = 500) -> List[ToolSpec]:
|
||||
"""Fetch and normalize the server's tool surface into [`ToolSpec`]s."""
|
||||
listing = client.list_tools(limit=limit)
|
||||
tools = listing.get("tools", []) if isinstance(listing, dict) else []
|
||||
specs: List[ToolSpec] = []
|
||||
for entry in tools:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = entry.get("name")
|
||||
if not isinstance(name, str) or not name:
|
||||
continue
|
||||
description = entry.get("description")
|
||||
if not isinstance(description, str):
|
||||
description = ""
|
||||
schema = (
|
||||
entry.get("input_schema")
|
||||
or entry.get("inputSchema")
|
||||
or entry.get("parameters")
|
||||
or _default_schema()
|
||||
)
|
||||
if not isinstance(schema, dict):
|
||||
schema = _default_schema()
|
||||
specs.append(ToolSpec(name=name, description=description, parameters=schema))
|
||||
return specs
|
||||
|
||||
|
||||
def coerce_arguments(raw: Any) -> Dict[str, Any]:
|
||||
"""Coerce tool-call arguments (JSON string or dict) into a dict."""
|
||||
if raw is None:
|
||||
return {}
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
if isinstance(raw, str):
|
||||
text = raw.strip()
|
||||
if not text:
|
||||
return {}
|
||||
parsed = json.loads(text)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
return {}
|
||||
|
||||
|
||||
def make_json_runner(client: LeanCtxClient, name: str) -> Callable[[str], str]:
|
||||
"""A `(arguments: str) -> str` runner used by string-input frameworks.
|
||||
|
||||
The single JSON-string argument is the most portable shape across framework
|
||||
versions and avoids brittle per-tool signature synthesis.
|
||||
"""
|
||||
|
||||
def run(arguments: str = "") -> str:
|
||||
return client.call_tool_text(name, coerce_arguments(arguments))
|
||||
|
||||
run.__name__ = name
|
||||
run.__doc__ = f"Invoke the lean-ctx tool '{name}' with a JSON object string."
|
||||
return run
|
||||
@@ -0,0 +1,53 @@
|
||||
"""CrewAI adapter (EPIC 12.6).
|
||||
|
||||
Exposes lean-ctx tools as CrewAI `BaseTool`s. The framework is an optional
|
||||
dependency, imported lazily.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from ..client import LeanCtxClient
|
||||
from ._common import coerce_arguments, normalized_tool_specs
|
||||
|
||||
|
||||
def to_crewai_tools(client: LeanCtxClient) -> List[Any]:
|
||||
"""Return lean-ctx tools as CrewAI `BaseTool` instances.
|
||||
|
||||
Each tool takes a single ``arguments`` field: a JSON object string of the
|
||||
tool's arguments. This is portable across CrewAI versions and avoids
|
||||
synthesizing a distinct pydantic schema per tool.
|
||||
"""
|
||||
try:
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
except ImportError as exc: # pragma: no cover - exercised only without dep
|
||||
raise ImportError(
|
||||
"CrewAI is required for this adapter: pip install crewai"
|
||||
) from exc
|
||||
|
||||
class _ArgsSchema(BaseModel):
|
||||
arguments: str = Field(
|
||||
default="",
|
||||
description="JSON object string of the tool's arguments.",
|
||||
)
|
||||
|
||||
class _LeanCtxTool(BaseTool):
|
||||
args_schema: type[BaseModel] = _ArgsSchema
|
||||
|
||||
def __init__(self, tool_name: str, tool_description: str) -> None:
|
||||
super().__init__(name=tool_name, description=tool_description)
|
||||
self._tool_name = tool_name
|
||||
|
||||
def _run(self, arguments: str = "") -> str:
|
||||
return client.call_tool_text(self._tool_name, coerce_arguments(arguments))
|
||||
|
||||
tools: List[Any] = []
|
||||
for spec in normalized_tool_specs(client):
|
||||
description = (
|
||||
f"{spec.description} "
|
||||
"Input: a JSON object string matching this tool's argument schema."
|
||||
).strip()
|
||||
tools.append(_LeanCtxTool(spec.name, description))
|
||||
return tools
|
||||
@@ -0,0 +1,36 @@
|
||||
"""LangChain adapter (EPIC 12.6).
|
||||
|
||||
Exposes lean-ctx tools as LangChain `Tool`s. The framework is an optional
|
||||
dependency, imported lazily so the SDK has no hard coupling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from ..client import LeanCtxClient
|
||||
from ._common import make_json_runner, normalized_tool_specs
|
||||
|
||||
|
||||
def to_langchain_tools(client: LeanCtxClient) -> List[Any]:
|
||||
"""Return lean-ctx tools as `langchain_core.tools.Tool` instances.
|
||||
|
||||
Each tool accepts a JSON object string of arguments — the most portable
|
||||
shape across LangChain versions.
|
||||
"""
|
||||
try:
|
||||
from langchain_core.tools import Tool
|
||||
except ImportError as exc: # pragma: no cover - exercised only without dep
|
||||
raise ImportError(
|
||||
"LangChain is required for this adapter: pip install langchain-core"
|
||||
) from exc
|
||||
|
||||
tools: List[Any] = []
|
||||
for spec in normalized_tool_specs(client):
|
||||
runner = make_json_runner(client, spec.name)
|
||||
description = (
|
||||
f"{spec.description} "
|
||||
"Input: a JSON object string matching this tool's argument schema."
|
||||
).strip()
|
||||
tools.append(Tool(name=spec.name, description=description, func=runner))
|
||||
return tools
|
||||
@@ -0,0 +1,36 @@
|
||||
"""LlamaIndex adapter (EPIC 12.6).
|
||||
|
||||
Exposes lean-ctx tools as LlamaIndex `FunctionTool`s. The framework is an
|
||||
optional dependency, imported lazily.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from ..client import LeanCtxClient
|
||||
from ._common import make_json_runner, normalized_tool_specs
|
||||
|
||||
|
||||
def to_llamaindex_tools(client: LeanCtxClient) -> List[Any]:
|
||||
"""Return lean-ctx tools as `llama_index.core.tools.FunctionTool` instances."""
|
||||
try:
|
||||
from llama_index.core.tools import FunctionTool
|
||||
except ImportError as exc: # pragma: no cover - exercised only without dep
|
||||
raise ImportError(
|
||||
"LlamaIndex is required for this adapter: pip install llama-index-core"
|
||||
) from exc
|
||||
|
||||
tools: List[Any] = []
|
||||
for spec in normalized_tool_specs(client):
|
||||
runner = make_json_runner(client, spec.name)
|
||||
description = (
|
||||
f"{spec.description} "
|
||||
"Input: a JSON object string matching this tool's argument schema."
|
||||
).strip()
|
||||
tools.append(
|
||||
FunctionTool.from_defaults(
|
||||
fn=runner, name=spec.name, description=description
|
||||
)
|
||||
)
|
||||
return tools
|
||||
@@ -0,0 +1,46 @@
|
||||
"""OpenAI function-calling adapter (EPIC 12.6).
|
||||
|
||||
Pure transformation — no `openai` package required. Turns the lean-ctx tool
|
||||
surface into OpenAI ``tools=[...]`` specs and executes the tool calls the model
|
||||
returns. Works with both the Chat Completions and Responses tool-call shapes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ..client import LeanCtxClient
|
||||
from ._common import coerce_arguments, normalized_tool_specs
|
||||
|
||||
|
||||
def to_openai_tools(client: LeanCtxClient) -> List[Dict[str, Any]]:
|
||||
"""Return lean-ctx tools as OpenAI function-tool specs."""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": spec.name,
|
||||
"description": spec.description,
|
||||
"parameters": spec.parameters,
|
||||
},
|
||||
}
|
||||
for spec in normalized_tool_specs(client)
|
||||
]
|
||||
|
||||
|
||||
def run_openai_tool_call(client: LeanCtxClient, tool_call: Any) -> str:
|
||||
"""Execute one OpenAI tool call and return the tool's text result.
|
||||
|
||||
Accepts either the dict shape (``{"function": {"name", "arguments"}}``) or
|
||||
an SDK object exposing ``.function.name`` / ``.function.arguments``.
|
||||
"""
|
||||
function = tool_call.get("function") if isinstance(tool_call, dict) else getattr(tool_call, "function", None)
|
||||
if function is None:
|
||||
raise ValueError("tool_call has no 'function'")
|
||||
name = function.get("name") if isinstance(function, dict) else getattr(function, "name", None)
|
||||
if not name:
|
||||
raise ValueError("tool_call.function has no 'name'")
|
||||
raw_args = (
|
||||
function.get("arguments") if isinstance(function, dict) else getattr(function, "arguments", None)
|
||||
)
|
||||
return client.call_tool_text(name, coerce_arguments(raw_args))
|
||||
Reference in New Issue
Block a user