Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
3020e31
Merge pull request #122 from rootcodelabs/wip
nuwangeek Feb 20, 2026
6e5c22c
remove unwanted file
nuwangeek Feb 20, 2026
38d0533
updated changes
nuwangeek Feb 20, 2026
72b8ae1
fixed requested changes
nuwangeek Feb 20, 2026
9b7bc7b
fixed issue
nuwangeek Feb 20, 2026
46dd6c4
Merge pull request #123 from rootcodelabs/llm-316
nuwangeek Feb 21, 2026
068f4e0
Merge pull request #124 from buerokratt/wip
Thirunayan22 Feb 21, 2026
a2084e5
service workflow implementation without calling service endpoints
nuwangeek Feb 24, 2026
5216c09
Merge pull request #126 from rootcodelabs/wip
nuwangeek Feb 24, 2026
864ad30
fixed requested changes
nuwangeek Feb 24, 2026
25f9614
fixed issues
nuwangeek Feb 24, 2026
69c1279
protocol related requested changes
nuwangeek Feb 24, 2026
07f2e0f
fixed requested changes
nuwangeek Feb 24, 2026
f63f777
update time tracking
nuwangeek Feb 25, 2026
5429bc0
added time tracking and reloacate input guardrail before toolclassifiier
nuwangeek Feb 25, 2026
721263a
fixed issue
nuwangeek Feb 25, 2026
6ed02d1
Merge pull request #127 from buerokratt/wip
nuwangeek Feb 25, 2026
7238baa
Merge branch 'optimization/llm-304' into wip
nuwangeek Feb 25, 2026
ae7cfa0
Merge pull request #128 from rootcodelabs/wip
nuwangeek Feb 25, 2026
f8a82b6
fixed issue
nuwangeek Feb 25, 2026
3b89fba
added hybrid search for the service detection
nuwangeek Feb 26, 2026
789f062
update tool classifier
nuwangeek Mar 1, 2026
609e6d5
fixing merge conflicts
nuwangeek Mar 1, 2026
a30c52d
Merge pull request #129 from buerokratt/wip
nuwangeek Mar 1, 2026
8dfc155
Merge pull request #130 from rootcodelabs/wip
nuwangeek Mar 1, 2026
3d7fb85
updated intent data enrichment and service classification flow perfor…
nuwangeek Mar 2, 2026
bee9fbf
fixed issue
nuwangeek Mar 2, 2026
4888045
Merge pull request #131 from rootcodelabs/optimization/data-enrichment
nuwangeek Mar 3, 2026
0a0806f
optimize first user query response generation time
nuwangeek Mar 3, 2026
1eb8b47
fixed pr reviewed issues
nuwangeek Mar 3, 2026
94b4f39
Merge pull request #132 from buerokratt/wip
nuwangeek Mar 3, 2026
82b3fe5
Merge branch 'optimization/vector-indexer' into wip
nuwangeek Mar 3, 2026
1b4ada9
Merge pull request #134 from buerokratt/wip
nuwangeek Mar 3, 2026
bb1601f
service integration
nuwangeek Mar 8, 2026
9ce1da2
context based response generation flow
nuwangeek Mar 9, 2026
d647f86
fixed pr review suggested issues
nuwangeek Mar 9, 2026
d67214e
Merge pull request #135 from rootcodelabs/llm-309
nuwangeek Mar 9, 2026
b90ab52
Merge pull request #136 from rootcodelabs/llm-310
nuwangeek Mar 9, 2026
6c46d3c
removed service project layer
nuwangeek Mar 10, 2026
d3e1494
fixed issues
nuwangeek Mar 12, 2026
4add446
Merge pull request #137 from rootcodelabs/llm-310
nuwangeek Mar 12, 2026
c2ef115
delete unnessary files
nuwangeek Mar 13, 2026
97f6f1a
added requested changes
nuwangeek Mar 13, 2026
0be284e
Merge pull request #138 from buerokratt/wip
nuwangeek Mar 17, 2026
a32ca6d
Merge branch 'llm/service-integration' into wip
nuwangeek Mar 17, 2026
4276e7d
Merge pull request #140 from buerokratt/wip
nuwangeek Mar 18, 2026
24259a9
Merge pull request #141 from buerokratt/wip
nuwangeek Mar 20, 2026
1a54c5d
Merge pull request #143 from buerokratt/wip
nuwangeek Mar 23, 2026
95dc35a
Merge pull request #145 from buerokratt/wip
nuwangeek Mar 26, 2026
e047f41
Merge pull request #150 from buerokratt/wip
nuwangeek Apr 7, 2026
0486fa4
fix issue in prompt config toggle
nuwangeek Apr 7, 2026
9e9bd77
Merge pull request #151 from buerokratt/wip
nuwangeek Apr 8, 2026
2bf97f8
Merge branch 'llm-382' into wip
nuwangeek Apr 8, 2026
2f53999
Merge pull request #153 from buerokratt/wip
nuwangeek Apr 8, 2026
876889d
Intergartion of CKB import API for agency data sync
ruwinirathnamalala Apr 10, 2026
b710853
Intergartion of CKB import API for agency data sync
ruwinirathnamalala Apr 10, 2026
e31a0af
Merge pull request #154 from buerokratt/wip
nuwangeek Apr 16, 2026
f6a4300
Merge pull request #156 from buerokratt/wip
nuwangeek Apr 16, 2026
2806221
Merge branch 'wip' of https://github.com/rootcodelabs/LLM-Module into…
ruwinirathnamalala Apr 17, 2026
1fc3b9c
standalone agentic loop module
nuwangeek Apr 17, 2026
622c969
fixed requested changes
nuwangeek Apr 17, 2026
cf9723e
fixed ruff format issues
nuwangeek Apr 17, 2026
d159731
Merge pull request #157 from rootcodelabs/llm-394
nuwangeek Apr 22, 2026
83c7500
complete API semantic searcher with ambiguous result handling and too…
nuwangeek Apr 22, 2026
21c3c27
Merge pull request #158 from rootcodelabs/llm-394
nuwangeek Apr 22, 2026
591b119
Merge pull request #159 from rootcodelabs/llm-345-dev
nuwangeek Apr 22, 2026
c5582f8
complete semantic searcher evaluation and update to multi point index…
nuwangeek Apr 22, 2026
f569070
Merge pull request #160 from rootcodelabs/llm-403
nuwangeek Apr 22, 2026
80bfce7
competed integration of agentic loop with semantic searcher and strea…
nuwangeek Apr 22, 2026
8b984f1
Enhancements in data-sync flow and updated agency_id in agency_sync t…
ruwinirathnamalala Apr 23, 2026
d71a5eb
Merge pull request #161 from rootcodelabs/llm-408
nuwangeek Apr 24, 2026
6efe48b
Implemented the API caller module
nuwangeek Apr 24, 2026
51d8a0e
Completed integration of CKB and RAG changelogs with schema updates f…
nuwangeek Apr 28, 2026
2449472
Merge pull request #164 from buerokratt/wip
nuwangeek May 5, 2026
43e9ad3
Merge branch 'llm-345-dev' into wip
nuwangeek May 5, 2026
0ea073b
Merge pull request #167 from buerokratt/wip
nuwangeek May 6, 2026
c368cfd
Merge pull request #169 from buerokratt/wip
nuwangeek May 6, 2026
bdc878c
Merge pull request #171 from buerokratt/wip
nuwangeek May 6, 2026
a385166
Merge branch 'llm-348' into wip
nuwangeek May 6, 2026
59b604c
Merge pull request #173 from buerokratt/wip
nuwangeek May 6, 2026
49e9e77
Merge pull request #175 from buerokratt/wip
nuwangeek May 6, 2026
8e7ab98
Merge branch 'ckb_integration_for_data_sync' into wip
nuwangeek May 6, 2026
5b3ee08
Merge pull request #177 from buerokratt/wip
nuwangeek May 7, 2026
135ddf0
Merge branch 'llm-412' into wip
nuwangeek May 7, 2026
3671b6c
Merge pull request #179 from buerokratt/wip
nuwangeek May 7, 2026
e8d2d8d
Merge pull request #180 from buerokratt/wip
nuwangeek May 8, 2026
019716f
completed unit testing for api tool calling
nuwangeek May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 98 additions & 3 deletions tests/test_api_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# Helpers
# ---------------------------------------------------------------------------

_URL = "http://api.example.com/endpoint"
_URL = "https://openholidaysapi.org/PublicHolidays"
_URL_B = "http://api.other.com/resource"
_GET_PARAMS = {"country": "EE", "year": "2024"}
_POST_PARAMS = {"firstName": "Test", "lastName": "User"}
Expand Down Expand Up @@ -214,7 +214,6 @@ async def test_400_returns_error_with_body(self) -> None:
assert result.is_server_error is False
assert result.response_data == error_body
assert result.error is not None
assert "Invalid date format" in result.error or str(error_body) in result.error

@pytest.mark.asyncio
async def test_404_with_plain_text_body(self) -> None:
Expand All @@ -230,7 +229,7 @@ async def test_404_with_plain_text_body(self) -> None:
assert result.status_code == 404
assert result.is_client_error is True
assert result.response_data == "Not Found"
assert result.error == "Not Found"
assert result.error is not None # localized message for 404

@pytest.mark.asyncio
async def test_422_with_plain_text_body(self) -> None:
Expand Down Expand Up @@ -546,3 +545,99 @@ def test_api_call_result_status_code_zero_is_not_client_or_server_error(
)
assert result.is_client_error is False
assert result.is_server_error is False


# ---------------------------------------------------------------------------
# CircuitBreaker edge cases
# ---------------------------------------------------------------------------


class TestCircuitBreakerEdgeCases:
def test_exact_threshold_crossing_opens_on_third_failure(self) -> None:
"""failure_threshold=3 → CLOSED after 2 failures, OPEN on the 3rd."""
cb = CircuitBreaker(failure_threshold=3, cooldown_seconds=60.0)

cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_CLOSED # 1st failure → still CLOSED

cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_CLOSED # 2nd failure → still CLOSED

cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_OPEN # 3rd failure → OPEN

def test_half_open_recovery_closes_breaker(self) -> None:
"""HALF_OPEN → record_success() → CLOSED with failure count reset."""
cb = CircuitBreaker(failure_threshold=2, cooldown_seconds=0.0)

cb.record_failure(_URL)
cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_OPEN

# Cooldown elapsed (0.0 s) → probe allowed → HALF_OPEN
cb.can_execute(_URL)
assert cb.get_state(_URL) == CB_STATE_HALF_OPEN

cb.record_success(_URL)
assert cb.get_state(_URL) == CB_STATE_CLOSED
# Verify state is fully reset
assert cb._get_state(_URL).failure_count == 0

def test_half_open_re_failure_reopens_breaker(self) -> None:
"""HALF_OPEN → record_failure() → OPEN again."""
cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=0.0)
cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_OPEN

cb.can_execute(_URL) # Cooldown elapsed → HALF_OPEN
assert cb.get_state(_URL) == CB_STATE_HALF_OPEN

cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_OPEN

def test_cooldown_timing_keeps_open_until_elapsed(self) -> None:
"""OPEN breaker stays OPEN when cooldown has NOT yet elapsed."""
cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=999.0)
cb.record_failure(_URL)
assert cb.get_state(_URL) == CB_STATE_OPEN

# Time has NOT advanced → still OPEN
assert cb.can_execute(_URL) is False
assert cb.get_state(_URL) == CB_STATE_OPEN

def test_half_open_probe_in_flight_blocks_concurrent_callers(self) -> None:
"""Only one probe is allowed through HALF_OPEN; second call is blocked."""
cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=0.0)
cb.record_failure(_URL)

# First probe allowed → HALF_OPEN with probe_in_flight=True
first = cb.can_execute(_URL)
assert first is True
assert cb.get_state(_URL) == CB_STATE_HALF_OPEN

# Second call blocked while probe in flight
second = cb.can_execute(_URL)
assert second is False

@pytest.mark.asyncio
async def test_full_caller_recovery_after_cooldown(self) -> None:
"""APICaller: fail → OPEN → cooldown → probe succeeds → CLOSED."""
caller = APICaller(failure_threshold=1, cooldown_seconds=0.0)
fail_client = _make_client(_make_response(500))
success_client = _make_client(_make_response(200, json_data={"ok": True}))

# Trip the breaker
with patch(
"tool_classifier.api_caller.httpx.AsyncClient", return_value=fail_client
):
await caller.call(_URL, "GET", {})
assert caller._circuit_breaker.get_state(_URL) == CB_STATE_OPEN

# After 0-second cooldown, probe is allowed
with patch(
"tool_classifier.api_caller.httpx.AsyncClient", return_value=success_client
):
result = await caller.call(_URL, "GET", {})

assert result.success is True
assert caller._circuit_breaker.get_state(_URL) == CB_STATE_CLOSED
109 changes: 107 additions & 2 deletions tests/test_api_response_formatter.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""Unit tests for APIResponseFormatterModule — DSPy JSON-to-natural-language formatter."""

import json
from collections.abc import Generator
from collections.abc import AsyncIterator, Generator
from unittest.mock import MagicMock, patch

import dspy
import pytest

from src.tool_classifier.api_response_formatter import APIResponseFormatterModule
from src.tool_classifier.api_response_formatter import (
APIResponseFormatterModule,
_FORMATTER_ERROR_MESSAGES,
)


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -352,3 +355,105 @@ def test_forward_handles_prediction_error(self) -> None:

assert isinstance(result, str)
assert len(result) > 0


# ---------------------------------------------------------------------------
# stream_forward
# ---------------------------------------------------------------------------


class TestStreamForward:
"""Tests for stream_forward() on APIResponseFormatterModule."""

@pytest.mark.asyncio
async def test_yields_tokens_from_stream_response(self) -> None:
"""stream_forward() should yield token strings from the DSPy stream."""
formatter = APIResponseFormatterModule()
tokens_emitted = ["Estonia ", "has ", "10 ", "holidays."]

async def _fake_stream(
*args: object, **kwargs: object
) -> AsyncIterator[object]:
from dspy.streaming import StreamResponse

for token in tokens_emitted:
yield StreamResponse(
predict_name="formatter",
signature_field_name="formatted_answer",
chunk=token,
is_last_chunk=False,
)

yield dspy.Prediction(formatted_answer="Estonia has 10 holidays.")

with patch.object(
formatter, "_get_stream_predictor", return_value=_fake_stream
):
collected = [
token
async for token in formatter.stream_forward(
user_query="How many holidays in Estonia?",
api_response={"count": 10},
endpoint_description="Returns public holidays",
detected_language="en",
)
]

assert collected == tokens_emitted

@pytest.mark.asyncio
async def test_exception_in_stream_yields_localized_error(self) -> None:
"""When the stream predictor raises, stream_forward yields the localized error.

forward() is NOT called — the exception is caught directly and the
localized error message is yielded instead.
"""
formatter = APIResponseFormatterModule()

async def _raise_stream(
*args: object, **kwargs: object
) -> AsyncIterator[object]:
raise RuntimeError("Streaming unavailable")
yield # noqa: F821

forward_mock = MagicMock(return_value="Should not be called")
formatter._get_stream_predictor = MagicMock(return_value=_raise_stream)
formatter.forward = forward_mock

collected = [
token
async for token in formatter.stream_forward(
user_query="Holidays?",
api_response={"holidays": []},
endpoint_description="Returns public holidays",
detected_language="en",
)
]

assert collected == [_FORMATTER_ERROR_MESSAGES["en"]]
forward_mock.assert_not_called()

@pytest.mark.asyncio
async def test_yields_error_message_on_total_failure(self) -> None:
"""When streaming raises, yield the localized error message for the given language."""
formatter = APIResponseFormatterModule()

async def _raise_stream(
*args: object, **kwargs: object
) -> AsyncIterator[object]:
raise RuntimeError("Streaming unavailable")
yield # noqa: F821

formatter._get_stream_predictor = MagicMock(return_value=_raise_stream)

collected = [
token
async for token in formatter.stream_forward(
user_query="Holidays?",
api_response={"holidays": []},
endpoint_description="Returns public holidays",
detected_language="en",
)
]

assert collected == [_FORMATTER_ERROR_MESSAGES["en"]]
Loading
Loading