26382a7ac6
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
157 lines
4.9 KiB
Python
157 lines
4.9 KiB
Python
"""Tests for the framework adapters.
|
|
|
|
The OpenAI adapter is a pure transformation and is fully tested against a stub
|
|
server. The framework adapters (LangChain/LlamaIndex/CrewAI) are optional deps;
|
|
each test runs the adapter if the framework is importable, otherwise asserts the
|
|
helpful ImportError — so the suite is deterministic with or without them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from typing import Any, Tuple
|
|
|
|
import pytest
|
|
|
|
from leanctx import LeanCtxClient
|
|
from leanctx.adapters import (
|
|
coerce_arguments,
|
|
normalized_tool_specs,
|
|
run_openai_tool_call,
|
|
to_crewai_tools,
|
|
to_langchain_tools,
|
|
to_llamaindex_tools,
|
|
to_openai_tools,
|
|
)
|
|
|
|
TOOLS = [
|
|
{
|
|
"name": "ctx_read",
|
|
"description": "Read a file",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {"path": {"type": "string"}},
|
|
"required": ["path"],
|
|
},
|
|
},
|
|
{"name": "ctx_tree", "description": "List a directory", "input_schema": {"type": "object"}},
|
|
]
|
|
|
|
|
|
class _Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *args: Any) -> None:
|
|
pass
|
|
|
|
def _send(self, status: int, body: Any) -> None:
|
|
payload = json.dumps(body).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
|
|
def do_GET(self) -> None: # noqa: N802
|
|
if self.path.startswith("/v1/tools"):
|
|
self._send(200, {"tools": TOOLS, "total": len(TOOLS), "offset": 0, "limit": 500})
|
|
else:
|
|
self._send(404, {"error": "unknown"})
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
body = json.loads(self.rfile.read(length) or b"{}")
|
|
text = f"called {body.get('name')} with {json.dumps(body.get('arguments', {}), sort_keys=True)}"
|
|
self._send(200, {"result": {"content": [{"type": "text", "text": text}]}})
|
|
|
|
|
|
@pytest.fixture()
|
|
def client() -> Tuple[LeanCtxClient, HTTPServer]:
|
|
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
host, port = httpd.server_address
|
|
yield LeanCtxClient(f"http://{host}:{port}"), httpd
|
|
httpd.shutdown()
|
|
|
|
|
|
def test_normalized_specs(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
specs = normalized_tool_specs(c)
|
|
assert [s.name for s in specs] == ["ctx_read", "ctx_tree"]
|
|
assert specs[0].parameters["required"] == ["path"]
|
|
|
|
|
|
def test_to_openai_tools_shape(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
tools = to_openai_tools(c)
|
|
assert tools[0]["type"] == "function"
|
|
assert tools[0]["function"]["name"] == "ctx_read"
|
|
assert tools[0]["function"]["parameters"]["properties"]["path"]["type"] == "string"
|
|
|
|
|
|
def test_run_openai_tool_call_dict_and_object(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
# dict shape with JSON-string arguments
|
|
out = run_openai_tool_call(
|
|
c, {"function": {"name": "ctx_read", "arguments": '{"path": "README.md"}'}}
|
|
)
|
|
assert out == 'called ctx_read with {"path": "README.md"}'
|
|
|
|
# object shape with dict arguments
|
|
class _Fn:
|
|
name = "ctx_tree"
|
|
arguments = {"path": "."}
|
|
|
|
class _Call:
|
|
function = _Fn()
|
|
|
|
out2 = run_openai_tool_call(c, _Call())
|
|
assert out2 == 'called ctx_tree with {"path": "."}'
|
|
|
|
|
|
def test_coerce_arguments() -> None:
|
|
assert coerce_arguments(None) == {}
|
|
assert coerce_arguments("") == {}
|
|
assert coerce_arguments('{"a": 1}') == {"a": 1}
|
|
assert coerce_arguments({"a": 1}) == {"a": 1}
|
|
assert coerce_arguments("[1,2]") == {} # non-object JSON -> empty
|
|
|
|
|
|
def _has(mod: str) -> bool:
|
|
# find_spec raises (not returns None) when a parent package is absent.
|
|
try:
|
|
return importlib.util.find_spec(mod) is not None
|
|
except ModuleNotFoundError:
|
|
return False
|
|
|
|
|
|
def test_langchain_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
if _has("langchain_core"):
|
|
tools = to_langchain_tools(c)
|
|
assert len(tools) == 2
|
|
else:
|
|
with pytest.raises(ImportError, match="langchain-core"):
|
|
to_langchain_tools(c)
|
|
|
|
|
|
def test_llamaindex_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
if _has("llama_index.core"):
|
|
tools = to_llamaindex_tools(c)
|
|
assert len(tools) == 2
|
|
else:
|
|
with pytest.raises(ImportError, match="llama-index-core"):
|
|
to_llamaindex_tools(c)
|
|
|
|
|
|
def test_crewai_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
|
c, _ = client
|
|
if _has("crewai"):
|
|
tools = to_crewai_tools(c)
|
|
assert len(tools) == 2
|
|
else:
|
|
with pytest.raises(ImportError, match="crewai"):
|
|
to_crewai_tools(c)
|