feat(chat): implement web chat interface with accessibility
Core implementation: - chat_app.py: Chainlit entry point with @cl.on_chat_start, @cl.on_message (streaming via llm.stream_complete), @cl.on_chat_end Reuses Config, KnowledgeBase, ConversationStore, AdminNotifier from src/ Handles registration completion, post-completion updates, new-child flow - src/llm.py: add stream_complete() generator (litellm stream=True) alongside existing complete(); tests added in tests/test_llm.py - src/agent/response_parser.py: extract parse_llm_response(), apply_updates(), fallback_message() from EmailAgent into shared module EmailAgent now delegates to these functions (no logic change) Chainlit configuration: - chainlit.toml: telemetry off, German default, custom CSS + JS paths - chainlit.md: German welcome page with playgroup info Accessibility (WCAG 2.1 AA): - public/custom.css: contrast overrides (≥4.5:1), prefers-reduced-motion (static "…" replaces animated dots), skip link styles, 100dvh fix - public/accessibility.js: MutationObserver injects aria-live="polite" on message list, focus management after agent replies, skip link element Other: - .gitignore: add .chainlit/ (Chainlit runtime, auto-generated) - openspec/config.yaml: populate context field with tech stack - openspec/changes/implement-web-chat/tasks.md: mark completed tasks 95 tests pass. https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
This commit is contained in:
@@ -65,3 +65,60 @@ class TestLlmComplete:
|
||||
|
||||
with pytest.raises(RuntimeError, match="API error"):
|
||||
llm.complete("anthropic/claude-opus-4-6", "system", [])
|
||||
|
||||
|
||||
class TestLlmStreamComplete:
|
||||
def _make_chunk(self, content):
|
||||
chunk = type("Chunk", (), {})()
|
||||
choice = type("Choice", (), {})()
|
||||
delta = type("Delta", (), {"content": content})()
|
||||
choice.delta = delta
|
||||
chunk.choices = [choice]
|
||||
return chunk
|
||||
|
||||
def test_yields_chunks(self, mocker):
|
||||
chunks = [self._make_chunk("Hal"), self._make_chunk("lo!")]
|
||||
mocker.patch("litellm.completion", return_value=iter(chunks))
|
||||
|
||||
result = list(llm.stream_complete("anthropic/claude-opus-4-6", "system", []))
|
||||
|
||||
assert result == ["Hal", "lo!"]
|
||||
|
||||
def test_skips_empty_deltas(self, mocker):
|
||||
chunks = [self._make_chunk("Hello"), self._make_chunk(None), self._make_chunk("!")]
|
||||
mocker.patch("litellm.completion", return_value=iter(chunks))
|
||||
|
||||
result = list(llm.stream_complete("anthropic/claude-opus-4-6", "system", []))
|
||||
|
||||
assert result == ["Hello", "!"]
|
||||
|
||||
def test_passes_stream_true(self, mocker):
|
||||
mock_completion = mocker.patch("litellm.completion", return_value=iter([]))
|
||||
|
||||
list(llm.stream_complete("anthropic/claude-opus-4-6", "system", []))
|
||||
|
||||
assert mock_completion.call_args.kwargs["stream"] is True
|
||||
|
||||
def test_passes_model(self, mocker):
|
||||
mock_completion = mocker.patch("litellm.completion", return_value=iter([]))
|
||||
|
||||
list(llm.stream_complete("openai/gpt-4o", "system", []))
|
||||
|
||||
assert mock_completion.call_args.kwargs["model"] == "openai/gpt-4o"
|
||||
|
||||
def test_system_prompt_prepended(self, mocker):
|
||||
mock_completion = mocker.patch("litellm.completion", return_value=iter([]))
|
||||
|
||||
list(llm.stream_complete("anthropic/claude-opus-4-6", "You are helpful.", []))
|
||||
|
||||
messages = mock_completion.call_args.kwargs["messages"]
|
||||
assert messages[0] == {"role": "system", "content": "You are helpful."}
|
||||
|
||||
def test_chat_messages_appended(self, mocker):
|
||||
mock_completion = mocker.patch("litellm.completion", return_value=iter([]))
|
||||
|
||||
chat = [ChatMessage(role="user", content="Hallo")]
|
||||
list(llm.stream_complete("anthropic/claude-opus-4-6", "system", chat))
|
||||
|
||||
messages = mock_completion.call_args.kwargs["messages"]
|
||||
assert messages[1] == {"role": "user", "content": "Hallo"}
|
||||
|
||||
Reference in New Issue
Block a user