mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 18:47:04 +01:00
Split the Claude backend out of code_tutor/_llm
_llm.py 295 -> 217, clearing the last code_tutor source violation; only its seven test modules remain over the cap. The seam falls between the two backends because each pins a different plain import: test_llm.py patches requests.post on _llm for OllamaBackend and anthropic.Anthropic for ClaudeBackend. _llm keeps its own anthropic import - complete_with_fallback still catches APIError - so the four anthropic patch strings were split per test rather than moved wholesale. Pass count is 217 before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
1d1938ce23
commit
eb322dcf80
@ -0,0 +1,44 @@
|
||||
{
|
||||
"intent": "Bring code_tutor/_llm.py under the 250-line cap by moving the Anthropic-API backend into its own module, clearing the last code_tutor source violation.",
|
||||
"scope": [
|
||||
"python_pkg/code_tutor/_claude_backend.py (new)",
|
||||
"python_pkg/code_tutor/_llm.py (295 -> 217)",
|
||||
"python_pkg/code_tutor/tests/test_llm.py (import and patch paths only)",
|
||||
"Non-goal: no assertion changed, no test deleted, skipped or weakened"
|
||||
],
|
||||
"changes": [
|
||||
"Moved ClaudeBackend into _claude_backend.py. The seam falls between the two backends precisely because each pins a different plain import: test_llm.py patches requests.post on _llm for OllamaBackend and anthropic.Anthropic for ClaudeBackend, so separating the classes separates the two pinned modules cleanly.",
|
||||
"_llm.py keeps its anthropic import, because complete_with_fallback still catches anthropic.APIError, and imports ClaudeBackend back since it constructs one.",
|
||||
"Repointed the four anthropic patch strings belonging to the ClaudeBackend tests. The two in complete_with_fallback tests stay on _llm, since that is the module whose globals the except clause resolves through."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "timeout 90 env PYTHONPATH=. python3 -m pytest python_pkg/code_tutor/tests -q",
|
||||
"result": "pass",
|
||||
"evidence": "217 passed - unchanged from the count established by the previous commit."
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -c \"from python_pkg.code_tutor import _llm as l, _claude_backend as c; l.requests; l.anthropic; l.ClaudeBackend; l.OllamaBackend; c.anthropic; c.ClaudeBackend\"",
|
||||
"result": "pass",
|
||||
"evidence": "resolve OK - anthropic is a real attribute of both modules, which is what makes the per-test split of its patch strings valid."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all | grep code_tutor",
|
||||
"result": "pass",
|
||||
"evidence": "Only test modules are listed now: every code_tutor source file is under the 250-line cap."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <the touched files and artifacts>",
|
||||
"result": "pass",
|
||||
"evidence": "ruff flagged E501 on a patch string the longer module name pushed past 88 columns; auto-formatted, then clean. pytest-with-coverage passes at 100%."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"anthropic is now imported by two modules and patched on both, for different reasons. A future change that stops complete_with_fallback catching APIError would make the _llm import unused, and ruff would strip it and break the two patches that remain there.",
|
||||
"_llm imports ClaudeBackend from _claude_backend; the reverse import would be a cycle."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; _llm.py returns to 295 lines and _claude_backend.py disappears.",
|
||||
"After rollback, re-run the package suite and confirm it still reports 217 passed."
|
||||
]
|
||||
}
|
||||
93
python_pkg/code_tutor/_claude_backend.py
Normal file
93
python_pkg/code_tutor/_claude_backend.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""The Anthropic-API backend used as a fallback when the local model is weak.
|
||||
|
||||
Split out of :mod:`python_pkg.code_tutor._llm` to keep it under the 250-line
|
||||
cap. The ``anthropic`` import travels with the only class that uses it, so
|
||||
``tests/test_llm.py`` patches it on this module and ``requests`` on ``_llm``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import anthropic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class ClaudeBackend:
|
||||
"""LLM backend that calls the Anthropic Claude API.
|
||||
|
||||
Reads ``ANTHROPIC_API_KEY`` from the environment automatically.
|
||||
|
||||
Args:
|
||||
model: Claude model identifier.
|
||||
max_tokens: Maximum tokens in the response.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "claude-sonnet-4-6",
|
||||
max_tokens: int = 1024,
|
||||
) -> None:
|
||||
"""Initialise Anthropic client."""
|
||||
self._client = anthropic.Anthropic()
|
||||
self._model = model
|
||||
self._max_tokens = max_tokens
|
||||
|
||||
def complete(self, system: str, user: str) -> str:
|
||||
"""Call Claude and return the response text.
|
||||
|
||||
Args:
|
||||
system: System prompt.
|
||||
user: User message.
|
||||
|
||||
Returns:
|
||||
Response string from Claude.
|
||||
|
||||
Raises:
|
||||
anthropic.APIError: On any Anthropic API failure.
|
||||
"""
|
||||
message = self._client.messages.create(
|
||||
model=self._model,
|
||||
max_tokens=self._max_tokens,
|
||||
system=system,
|
||||
messages=[{"role": "user", "content": user}],
|
||||
)
|
||||
block = message.content[0]
|
||||
if hasattr(block, "text"):
|
||||
return str(block.text)
|
||||
return ""
|
||||
|
||||
def stream(
|
||||
self,
|
||||
system: str,
|
||||
user: str,
|
||||
on_token: Callable[[str], None],
|
||||
) -> str:
|
||||
"""Call Claude with streaming, invoking *on_token* per text delta.
|
||||
|
||||
Falls back to ``complete`` on ``APIError``.
|
||||
|
||||
Args:
|
||||
system: System prompt.
|
||||
user: User message.
|
||||
on_token: Callback invoked with each text delta as it arrives.
|
||||
|
||||
Returns:
|
||||
Full accumulated response text.
|
||||
"""
|
||||
try:
|
||||
parts: list[str] = []
|
||||
with self._client.messages.stream(
|
||||
model=self._model,
|
||||
max_tokens=self._max_tokens,
|
||||
system=system,
|
||||
messages=[{"role": "user", "content": user}],
|
||||
) as s:
|
||||
for text in s.text_stream:
|
||||
on_token(text)
|
||||
parts.append(text)
|
||||
return "".join(parts)
|
||||
except anthropic.APIError:
|
||||
return self.complete(system, user)
|
||||
@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, Protocol
|
||||
import anthropic
|
||||
import requests
|
||||
|
||||
from python_pkg.code_tutor._claude_backend import ClaudeBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@ -157,84 +159,6 @@ class OllamaBackend:
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
class ClaudeBackend:
|
||||
"""LLM backend that calls the Anthropic Claude API.
|
||||
|
||||
Reads ``ANTHROPIC_API_KEY`` from the environment automatically.
|
||||
|
||||
Args:
|
||||
model: Claude model identifier.
|
||||
max_tokens: Maximum tokens in the response.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "claude-sonnet-4-6",
|
||||
max_tokens: int = 1024,
|
||||
) -> None:
|
||||
"""Initialise Anthropic client."""
|
||||
self._client = anthropic.Anthropic()
|
||||
self._model = model
|
||||
self._max_tokens = max_tokens
|
||||
|
||||
def complete(self, system: str, user: str) -> str:
|
||||
"""Call Claude and return the response text.
|
||||
|
||||
Args:
|
||||
system: System prompt.
|
||||
user: User message.
|
||||
|
||||
Returns:
|
||||
Response string from Claude.
|
||||
|
||||
Raises:
|
||||
anthropic.APIError: On any Anthropic API failure.
|
||||
"""
|
||||
message = self._client.messages.create(
|
||||
model=self._model,
|
||||
max_tokens=self._max_tokens,
|
||||
system=system,
|
||||
messages=[{"role": "user", "content": user}],
|
||||
)
|
||||
block = message.content[0]
|
||||
if hasattr(block, "text"):
|
||||
return str(block.text)
|
||||
return ""
|
||||
|
||||
def stream(
|
||||
self,
|
||||
system: str,
|
||||
user: str,
|
||||
on_token: Callable[[str], None],
|
||||
) -> str:
|
||||
"""Call Claude with streaming, invoking *on_token* per text delta.
|
||||
|
||||
Falls back to ``complete`` on ``APIError``.
|
||||
|
||||
Args:
|
||||
system: System prompt.
|
||||
user: User message.
|
||||
on_token: Callback invoked with each text delta as it arrives.
|
||||
|
||||
Returns:
|
||||
Full accumulated response text.
|
||||
"""
|
||||
try:
|
||||
parts: list[str] = []
|
||||
with self._client.messages.stream(
|
||||
model=self._model,
|
||||
max_tokens=self._max_tokens,
|
||||
system=system,
|
||||
messages=[{"role": "user", "content": user}],
|
||||
) as s:
|
||||
for text in s.text_stream:
|
||||
on_token(text)
|
||||
parts.append(text)
|
||||
return "".join(parts)
|
||||
except anthropic.APIError:
|
||||
return self.complete(system, user)
|
||||
|
||||
|
||||
def _is_weak(response: str) -> bool:
|
||||
"""Return True if *response* looks too short or expresses uncertainty.
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.code_tutor._claude_backend import ClaudeBackend
|
||||
from python_pkg.code_tutor._llm import (
|
||||
ClaudeBackend,
|
||||
OllamaBackend,
|
||||
_is_weak,
|
||||
complete_with_fallback,
|
||||
@ -94,7 +94,8 @@ def test_claude_complete_with_text_block() -> None:
|
||||
mock_client.messages.create.return_value = mock_message
|
||||
|
||||
with patch(
|
||||
"python_pkg.code_tutor._llm.anthropic.Anthropic", return_value=mock_client
|
||||
"python_pkg.code_tutor._claude_backend.anthropic.Anthropic",
|
||||
return_value=mock_client,
|
||||
):
|
||||
backend = ClaudeBackend()
|
||||
result = backend.complete("sys", "user")
|
||||
@ -111,7 +112,8 @@ def test_claude_complete_no_text_attr() -> None:
|
||||
mock_client.messages.create.return_value = mock_message
|
||||
|
||||
with patch(
|
||||
"python_pkg.code_tutor._llm.anthropic.Anthropic", return_value=mock_client
|
||||
"python_pkg.code_tutor._claude_backend.anthropic.Anthropic",
|
||||
return_value=mock_client,
|
||||
):
|
||||
backend = ClaudeBackend()
|
||||
result = backend.complete("sys", "user")
|
||||
@ -135,7 +137,8 @@ def test_claude_stream_success() -> None:
|
||||
|
||||
tokens: list[str] = []
|
||||
with patch(
|
||||
"python_pkg.code_tutor._llm.anthropic.Anthropic", return_value=mock_client
|
||||
"python_pkg.code_tutor._claude_backend.anthropic.Anthropic",
|
||||
return_value=mock_client,
|
||||
):
|
||||
backend = ClaudeBackend()
|
||||
result = backend.stream("sys", "user", tokens.append)
|
||||
@ -161,7 +164,8 @@ def test_claude_stream_api_error_fallback() -> None:
|
||||
|
||||
tokens: list[str] = []
|
||||
with patch(
|
||||
"python_pkg.code_tutor._llm.anthropic.Anthropic", return_value=mock_client
|
||||
"python_pkg.code_tutor._claude_backend.anthropic.Anthropic",
|
||||
return_value=mock_client,
|
||||
):
|
||||
backend = ClaudeBackend()
|
||||
result = backend.stream("sys", "user", tokens.append)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user