@pentatonic-ai/ai-agent-sdk 0.8.2 → 0.8.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -86,7 +86,12 @@ log = logging.getLogger("l6-document-store")
86
86
  # Embedding
87
87
  # ---------------------------------------------------------------------------
88
88
 
89
- _embed_client = httpx.Client(timeout=60)
89
+ # HTTP client for Ollama entity extraction (extract_entities below). Named
90
+ # `_ollama_http`, not `_embed_client`, because the embedding HTTP client now
91
+ # lives behind the EmbedClient factory above — sharing the `_embed_client`
92
+ # identifier caused a TypeError in v0.8.0–0.8.2 where the legacy module-level
93
+ # binding shadowed the factory function.
94
+ _ollama_http = httpx.Client(timeout=60)
90
95
 
91
96
  def embed_text(text: str) -> List[float]:
92
97
  """Single-text embed via _embed_post (OpenAI-compat first, lambda-gateway fallback)."""
@@ -141,7 +146,7 @@ def rerank(query: str, results: List[Dict], top_k: int = 10) -> List[Dict]:
141
146
  def extract_entities(text: str) -> List[str]:
142
147
  """Extract entities from text using Ollama graph-preflexor."""
143
148
  try:
144
- resp = _embed_client.post(
149
+ resp = _ollama_http.post(
145
150
  f"{OLLAMA_URL}/api/generate",
146
151
  json={
147
152
  "model": "graph-preflexor",
@@ -0,0 +1,84 @@
1
+ """Regression test for the L6 _embed_client shadowing bug introduced in v0.8.0.
2
+
3
+ When the EmbedClient refactor landed in 0.8.0, the new `def _embed_client()`
4
+ factory function was added at module top, but the legacy module-level
5
+ `_embed_client = httpx.Client(timeout=60)` binding (used by Ollama entity
6
+ extraction) was left in place. Python's top-to-bottom evaluation rebound
7
+ the name to the httpx.Client instance, so any subsequent call to
8
+ `_embed_client()` raised `TypeError: 'Client' object is not callable`.
9
+
10
+ This silently 500'd every L6 /index-batch and /search request from 0.8.0
11
+ through 0.8.2 — the bug couldn't be caught by /health because the process
12
+ itself stays up, only the request handlers fail.
13
+
14
+ This is a static-source test (parses the file) rather than an import-time
15
+ test because L6's heavy imports (pymilvus, spacy) aren't available in the
16
+ unit-test venv. The check: scan the AST for any non-function rebinding of
17
+ identifiers that are also defined as `def` in the same module. Catches
18
+ this exact bug shape across any service that uses the EmbedClient factory
19
+ pattern.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import ast
25
+ from pathlib import Path
26
+
27
+ import pytest
28
+
29
+ SERVICES_DIR = Path(__file__).parent.parent / "engine" / "services"
30
+
31
+ # Services that use the lazy EmbedClient factory pattern.
32
+ SERVICES_WITH_EMBED_FACTORY = [
33
+ SERVICES_DIR / "l4" / "server.py",
34
+ SERVICES_DIR / "l5" / "l5-comms-layer.py",
35
+ SERVICES_DIR / "l6" / "l6-document-store.py",
36
+ SERVICES_DIR / "l2" / "l2-hybridrag-proxy.py",
37
+ ]
38
+
39
+
40
+ def _module_level_defs_and_assigns(source: str) -> tuple[set[str], set[str]]:
41
+ """Return (function names, non-function-assigned names) at module level."""
42
+ tree = ast.parse(source)
43
+ funcs: set[str] = set()
44
+ assigns: set[str] = set()
45
+ for node in tree.body:
46
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
47
+ funcs.add(node.name)
48
+ elif isinstance(node, ast.Assign):
49
+ for target in node.targets:
50
+ if isinstance(target, ast.Name):
51
+ assigns.add(target.id)
52
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
53
+ assigns.add(node.target.id)
54
+ return funcs, assigns
55
+
56
+
57
+ @pytest.mark.parametrize("service_file", SERVICES_WITH_EMBED_FACTORY, ids=lambda p: f"{p.parent.name}/{p.name}")
58
+ def test_no_module_level_shadowing_of_factory_functions(service_file: Path):
59
+ """A module-level `def foo()` must not also have a module-level `foo = ...`
60
+ later in the file. That's exactly the shape that caused the v0.8.0 L6 bug."""
61
+ source = service_file.read_text()
62
+ funcs, assigns = _module_level_defs_and_assigns(source)
63
+ overlap = funcs & assigns
64
+ assert not overlap, (
65
+ f"{service_file.relative_to(SERVICES_DIR.parent.parent)} has module-level "
66
+ f"identifier(s) defined as both `def` and `name = ...`: {sorted(overlap)}. "
67
+ f"This causes silent name shadowing — the assignment wins and any call "
68
+ f"to {sorted(overlap)[0]}() raises TypeError at runtime."
69
+ )
70
+
71
+
72
+ def test_l6_uses_renamed_ollama_http_not_embed_client():
73
+ """Belt-and-suspenders: explicitly assert L6's Ollama HTTP client is at
74
+ `_ollama_http`, not `_embed_client`. If someone reintroduces the original
75
+ binding by accident, this test catches it without depending on AST traversal."""
76
+ source = (SERVICES_DIR / "l6" / "l6-document-store.py").read_text()
77
+ assert "_embed_client = httpx.Client" not in source, (
78
+ "L6 reintroduced the legacy `_embed_client = httpx.Client(...)` binding "
79
+ "that shadowed the EmbedClient factory in v0.8.0. Rename to _ollama_http."
80
+ )
81
+ assert "_ollama_http = httpx.Client" in source, (
82
+ "L6 is missing the renamed Ollama HTTP client (`_ollama_http = httpx.Client(...)`). "
83
+ "The Ollama entity-extraction call path needs an httpx.Client somewhere."
84
+ )