Merge semantica-agi/main into fix/779-record-decision-logging

This commit is contained in:
Sameer6305
2026-07-31 18:28:20 +05:30
5 changed files with 64 additions and 13 deletions
+7
View File
@@ -48,6 +48,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Preserves graceful fallback behavior: `record_decision()` remains optional and `upsert_memory()` continues without propagating the exception
- Added regression coverage in `tests/integrations/agno/test_shared_context.py` for both `store()` and `record_decision()` warning paths
- **`AgnoDecisionKit`/`AgnoKGToolkit` silently swallowed Agno tool registration failures** (#780, #818) by @Sameer6305 and @KaifAhmad1
- Removed the `try/except: pass` wrapped around `self.register(fn)` in both toolkits' `__init__`; when Agno is installed, a registration failure now propagates immediately instead of leaving the toolkit half-registered with no signal to the caller
- Graceful degradation when Agno isn't installed (`AGNO_AVAILABLE=False`) is unchanged — `_tools` is still populated so callers can introspect available tools without the package
- Fixed a related duplicate-entry bug: `self._tools` was appended to unconditionally *before* `register()` ran, which could double-count a tool when Agno's own `Toolkit.register()` also tracks it in `self._tools`
- This is a behavior change for callers that construct these toolkits expecting instantiation to always succeed — audited: no in-repo call site relies on the old silent-failure behavior
- Expanded `tests/integrations/agno/test_decision_kit.py` and `test_kg_toolkit.py` with coverage for registration invocation counts, failure propagation, graceful degradation, and no-duplicate-`_tools` assertions
- **MCP `handle_get_causal_chain` returned an empty-but-valid-looking response when both `CausalChainAnalyzer` and the graph fallback were unavailable** (#781, #817) by @Sameer6305 and @KaifAhmad1
- Returns an explicit `{"error": "Causal chain analysis is not supported on this graph backend", "chain": []}` instead of `{"chain": [], "count": 0, "direction": ...}`, letting clients distinguish "unsupported" from a legitimately empty chain
- The fallback path now introspects `graph.get_causal_chain`'s signature to forward `direction`/`max_depth` (or a `depth` kwarg, or nothing, depending on what the backend accepts) instead of always calling with just `decision_id`, matching the primary analyzer path's behavior
+3 -5
View File
@@ -123,12 +123,10 @@ class AgnoDecisionKit(_ToolkitBase): # type: ignore[misc]
tools_to_register.append(self.check_policy)
for fn in tools_to_register:
self._tools.append(fn)
if AGNO_AVAILABLE:
try:
self.register(fn)
except Exception:
pass
self.register(fn)
if fn not in self._tools:
self._tools.append(fn)
logger.info("AgnoDecisionKit initialised")
+3 -5
View File
@@ -122,12 +122,10 @@ class AgnoKGToolkit(_ToolkitBase): # type: ignore[misc]
self.export_subgraph,
]
for fn in tools_to_register:
self._tools.append(fn)
if AGNO_AVAILABLE:
try:
self.register(fn)
except Exception:
pass
self.register(fn)
if fn not in self._tools:
self._tools.append(fn)
logger.info("AgnoKGToolkit initialised (backend=%s)", graph_store_backend)
+26 -2
View File
@@ -8,7 +8,7 @@ import json
import sys
import types
import unittest
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
# ---------------------------------------------------------------------------
@@ -75,7 +75,31 @@ class TestAgnoDecisionKitInit(unittest.TestCase):
def test_tools_registered(self):
kit = AgnoDecisionKit(context=_make_context())
# Tools should be registered (Toolkit.register was called)
self.assertTrue(len(kit._tools) >= 5)
self.assertEqual(len(kit._tools), 6)
self.assertEqual(len(kit._tools), len(set(kit._tools)))
def test_registration_invoked(self):
with patch.object(AgnoDecisionKit, "register") as mock_register:
AgnoDecisionKit(context=_make_context())
self.assertEqual(mock_register.call_count, 6)
def test_registration_failure_propagates(self):
with patch.object(AgnoDecisionKit, "register", side_effect=RuntimeError("Registration failed")):
with self.assertRaises(RuntimeError):
AgnoDecisionKit(context=_make_context())
def test_graceful_degradation_when_agno_unavailable(self):
with patch("integrations.agno.decision_kit.AGNO_AVAILABLE", False):
with patch.object(AgnoDecisionKit, "register") as mock_register:
kit = AgnoDecisionKit(context=_make_context())
mock_register.assert_not_called()
self.assertEqual(len(kit._tools), 6)
self.assertEqual(len(kit._tools), len(set(kit._tools)))
def test_no_duplicate_tools(self):
kit = AgnoDecisionKit(context=_make_context())
self.assertEqual(len(kit._tools), len(set(kit._tools)))
self.assertEqual(len(kit._tools), 6)
def test_policy_tool_can_be_disabled(self):
kit = AgnoDecisionKit(context=_make_context(), enable_policy_check=False)
+25 -1
View File
@@ -129,7 +129,31 @@ class TestAgnoKGToolkitInit(unittest.TestCase):
def test_tools_registered(self):
kit = AgnoKGToolkit()
self.assertTrue(len(kit._tools) >= 7)
self.assertEqual(len(kit._tools), 7)
self.assertEqual(len(kit._tools), len(set(kit._tools)))
def test_registration_invoked(self):
with patch.object(AgnoKGToolkit, "register") as mock_register:
AgnoKGToolkit()
self.assertEqual(mock_register.call_count, 7)
def test_registration_failure_propagates(self):
with patch.object(AgnoKGToolkit, "register", side_effect=RuntimeError("Registration failed")):
with self.assertRaises(RuntimeError):
AgnoKGToolkit()
def test_graceful_degradation_when_agno_unavailable(self):
with patch("integrations.agno.kg_toolkit.AGNO_AVAILABLE", False):
with patch.object(AgnoKGToolkit, "register") as mock_register:
kit = AgnoKGToolkit()
mock_register.assert_not_called()
self.assertEqual(len(kit._tools), 7)
self.assertEqual(len(kit._tools), len(set(kit._tools)))
def test_no_duplicate_tools(self):
kit = AgnoKGToolkit()
self.assertEqual(len(kit._tools), len(set(kit._tools)))
self.assertEqual(len(kit._tools), 7)
def test_context_graph_attached(self):
ctx = MagicMock()