Skip to content

fix(api/chat): wire chat endpoint through LLMService.chat() — closes silent canned-response regression (#7047) - #7101

Merged
mrveiss merged 1 commit into
Dev_new_guifrom
issue-7047
May 6, 2026
Merged

mrveiss merged 1 commit into
Dev_new_guifrom
issue-7047

Conversation

@mrveiss

@mrveiss mrveiss commented May 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

P1 user-facing regression. Since #3185 (LLMInterface retirement on 2026-05-04), the chat endpoint has been silently returning a canned "I'm currently unable to generate a response. Please try again." string for every user request. Two bugs in one helper:

Bug 1 — broken module path

```python

api/chat.py:106 (before)

from llm_service import LLMService # ← module doesn't exist
```

Canonical path is `services.llm_service`. Import was inside `get_llm_service()`, so it deferred to first call — would have raised `ModuleNotFoundError` on the first chat request had bug 2 not masked it.

Bug 2 — hasattr guard around a deleted method

```python

api/chat.py:543 (before)

if hasattr(llm_service, "generate_response"): # always False post-#3185
return await llm_service.generate_response(...)
else:
return {"content": "I'm currently unable to..."}
```

`LLMService` doesn't expose `generate_response`. Every call took the else branch.

Fix

  • Lazy-import path corrected.
  • Body migrated to `llm_service.chat(messages=..., conversation_id=session_id, request_id=...)` returning `LLMResponse`. Response shape verified: `.content: str`, `.error: Optional[str]`.
  • User-facing fallback strings unchanged on error — internal reasons logged but never leaked.

Regression pins (5 tests added)

  • Happy path: `response.content` surfaces as result content
  • Args verified: `messages` / `conversation_id` / `request_id` passed through
  • LLM error: fallback message, internal error NOT leaked
  • Exception in `chat()`: fallback message, exception detail NOT leaked
  • Bug 1 pin: source string match on `from services.llm_service import LLMService` — fails if anyone reintroduces the broken import path
  • Bug 2 pin: `generate_response` mock with `AssertionError` side-effect — test fails if a future change reintroduces the legacy method call

```
$ python3 -m pytest autobot-backend/api/chat_generate_ai_response_test.py -xvs
============================== 5 passed in 2.80s ===============================
```

Out of scope (deferred per PR-discipline / different contexts)

The other sites flagged in #7047 (async_chat_workflow.py:344, modern_ai_integration.py:673, nl_database_service.py:549) need per-site verification — return-shape and call-context differ enough that batching them risks the verify-return-shape lesson again. #7047 stays open until those land.

Closes parts 1 + 2 of #7047

@github-actions

github-actions Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

…silent canned-response regression (#7047)

Two #3185 LLMInterface-retirement misses shipped silently in the same
helper, causing chat() to silently return a canned "I'm currently
unable to generate a response" string for every user request:

  Bug 1 — broken module path (api/chat.py:106):

    from llm_service import LLMService          # module never existed

    Canonical path is services.llm_service. The import was inside
    ``get_llm_service()`` so it deferred to first call rather than
    boot — silent until a chat request arrives, then ModuleNotFoundError.

  Bug 2 — feature-degradation guard around a stale method (api/chat.py:543):

    if hasattr(llm_service, "generate_response"):
        return await llm_service.generate_response(...)
    else:
        return {"content": "I'm currently unable to generate a response..."}

    LLMService doesn't expose generate_response (that was an LLMInterface
    method). The hasattr guard always took the else branch, so every
    request received the canned fallback regardless of whether LLMService
    was healthy. Major user-facing functional regression.

Fix
---
1. Update lazy import to ``services.llm_service.LLMService``.
2. Replace the hasattr/else dance with a real ``llm_service.chat(...)``
   call. Map LLMService's contract:
     - messages         ← llm_context (already OpenAI-format)
     - conversation_id  ← session_id   (per-conversation overrides)
     - request_id       ← request_id   (passes through **kwargs for tracing)
   Read response.error / response.content per LLMResponse's Pydantic
   model (verified shapes against services/llm_service.py:99-112).
3. User-facing fallback strings are unchanged on error — internal
   error reasons are logged via ``logger.warning`` but never leaked
   to the response.

Verification
------------
Added ``api/chat_generate_ai_response_test.py`` — 5 tests pinning the
contract:

  - happy path: response.content surfaces as result["content"]
  - chat() args: messages/conversation_id/request_id passed through
  - LLM returns error: fallback string, error reason NOT leaked to user
  - chat() raises: fallback string, exception detail NOT leaked
  - **regression pin** for the original bug: hasattr(llm_service,
    "generate_response") path can never silently swallow output again
    — test fails if a future change reintroduces it
  - **regression pin** for the import path: assert source contains
    ``from services.llm_service import LLMService``

  $ python3 -m pytest autobot-backend/api/chat_generate_ai_response_test.py -xvs
  ============================== 5 passed in 2.80s ===============================

Per the feedback memory I just saved (verify_return_shape_in_method_migration):
- LLMResponse.content: str          ✓ verified
- LLMResponse.error: Optional[str]  ✓ verified
- chat() return type:               LLMResponse (inspected via inspect.signature)

Closes #7047 (items 1 + 2). The remaining sites flagged in the issue
body (async_chat_workflow.py, modern_ai_integration.py,
nl_database_service.py) are tracked for separate per-site verification —
their contexts differ enough that batching them risks the same return-
shape miss this PR caught with tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mrveiss
mrveiss merged commit ff22ca6 into Dev_new_gui May 6, 2026
3 of 6 checks passed
@mrveiss
mrveiss deleted the issue-7047 branch May 6, 2026 19:22
mrveiss pushed a commit that referenced this pull request May 7, 2026
Promotes the existing private ``_build_mock_response`` in
``tests/fixtures/mocks.py`` to a public, fully-parameterized
``make_llm_response`` factory. The migration tests I authored across
PRs #7101 / #7118 / #7131 each rolled their own LLMResponse stub —
three different patterns for the same shape. Combined with 4
pre-existing patterns elsewhere, that's 7 ad-hoc mock styles.

Why this matters
----------------
The factory's primary value: returns the **real** ``LLMResponse`` from
``llm_interface_pkg.models`` when importable. If a future field rename
or type change happens on that dataclass, every test that uses the
factory fails at fixture-construction — instead of silently producing
wrong-shape mocks the way ``MagicMock(content=...)`` would. This is the
class of drift documented in
``feedback_verify_return_shape_in_method_migration.md`` (#7069 lesson).

Changes
-------
- ``tests/fixtures/mocks.py``: promoted ``_build_mock_response`` →
  public ``make_llm_response`` with keyword-only args (``content``,
  ``error``, ``model``, ``provider``). Underscore alias retained as
  a back-compat one-liner — no callers break.
- ``tests/fixtures/__init__.py``: re-exported.
- ``tests/fixtures/test_make_llm_response.py``: 7 contract tests
  pinning the factory's behavior — most importantly the
  ``isinstance(LLMResponse)`` check that catches future drift.
- Migrated my 2 in-repo callers off ``class _StubResponse``:
    - api/chat_generate_ai_response_test.py
    - orchestration/workflow_documentation_summary_test.py
  Both now import ``from tests.fixtures import make_llm_response`` and
  wrap it in a same-name pytest fixture so the existing test code
  doesn't need rewriting.

Out of scope (deliberately not in this PR)
------------------------------------------
The 4 pre-existing ad-hoc patterns (each has different historical
reasons — keeping the migration surface small):

  - tests/orchestration/test_subagent_dispatcher_reflection.py:31
    (``_make_llm_response`` factory)
  - tests/api/test_knowledge_grounding.py (5 inline ``MagicMock(content=...)``)

These can migrate incrementally; the canonical helper is now available
when someone touches those files.

PR #7131 (perf benchmarks, still open) deliberately not touched —
amending an open PR mid-flight invites conflicts. It can migrate after
this lands.

Verification
------------
  $ python3 -m pytest \
      tests/fixtures/test_make_llm_response.py \
      api/chat_generate_ai_response_test.py \
      orchestration/workflow_documentation_summary_test.py -xvs
  ============================== 19 passed in 2.58s ===============================

  - 7 new contract tests (factory behavior pinned)
  - 5 existing api/chat tests still green via canonical fixture
  - 7 existing workflow_documentation tests still green via canonical fixture

Closes #7134

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mrveiss added a commit that referenced this pull request May 7, 2026
…7139)

Promotes the existing private ``_build_mock_response`` in
``tests/fixtures/mocks.py`` to a public, fully-parameterized
``make_llm_response`` factory. The migration tests I authored across
PRs #7101 / #7118 / #7131 each rolled their own LLMResponse stub —
three different patterns for the same shape. Combined with 4
pre-existing patterns elsewhere, that's 7 ad-hoc mock styles.

Why this matters
----------------
The factory's primary value: returns the **real** ``LLMResponse`` from
``llm_interface_pkg.models`` when importable. If a future field rename
or type change happens on that dataclass, every test that uses the
factory fails at fixture-construction — instead of silently producing
wrong-shape mocks the way ``MagicMock(content=...)`` would. This is the
class of drift documented in
``feedback_verify_return_shape_in_method_migration.md`` (#7069 lesson).

Changes
-------
- ``tests/fixtures/mocks.py``: promoted ``_build_mock_response`` →
  public ``make_llm_response`` with keyword-only args (``content``,
  ``error``, ``model``, ``provider``). Underscore alias retained as
  a back-compat one-liner — no callers break.
- ``tests/fixtures/__init__.py``: re-exported.
- ``tests/fixtures/test_make_llm_response.py``: 7 contract tests
  pinning the factory's behavior — most importantly the
  ``isinstance(LLMResponse)`` check that catches future drift.
- Migrated my 2 in-repo callers off ``class _StubResponse``:
    - api/chat_generate_ai_response_test.py
    - orchestration/workflow_documentation_summary_test.py
  Both now import ``from tests.fixtures import make_llm_response`` and
  wrap it in a same-name pytest fixture so the existing test code
  doesn't need rewriting.

Out of scope (deliberately not in this PR)
------------------------------------------
The 4 pre-existing ad-hoc patterns (each has different historical
reasons — keeping the migration surface small):

  - tests/orchestration/test_subagent_dispatcher_reflection.py:31
    (``_make_llm_response`` factory)
  - tests/api/test_knowledge_grounding.py (5 inline ``MagicMock(content=...)``)

These can migrate incrementally; the canonical helper is now available
when someone touches those files.

PR #7131 (perf benchmarks, still open) deliberately not touched —
amending an open PR mid-flight invites conflicts. It can migrate after
this lands.

Verification
------------
  $ python3 -m pytest \
      tests/fixtures/test_make_llm_response.py \
      api/chat_generate_ai_response_test.py \
      orchestration/workflow_documentation_summary_test.py -xvs
  ============================== 19 passed in 2.58s ===============================

  - 7 new contract tests (factory behavior pinned)
  - 5 existing api/chat tests still green via canonical fixture
  - 7 existing workflow_documentation tests still green via canonical fixture

Closes #7134

Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant