Compare commits

...

5 Commits

Author SHA1 Message Date
Ryan Chen 1e4942e0b8 Allow pasting images into the chat input 2026-09-11 17:25:13 -04:00
ryan 2988482791 Merge pull request 'Add read timeouts to llama-server clients so hangs fail fast' (#43) from llama-timeouts into main
Reviewed-on: #43
2026-09-11 17:18:15 -04:00
Ryan Chen 56ad5f7046 Document git workflow (Gitea, tea CLI, kermit = commit) 2026-09-11 17:13:11 -04:00
Ryan Chen b361377088 Add read timeouts to llama-server clients so hangs fail fast
The OpenAI client defaults to a 600s read timeout, so a wedged
llama-server blocked requests for 10 minutes and never raised, which
meant with_fallbacks() to OpenAI never triggered. Set a per-chunk read
timeout (120s, configurable via LLAMA_TIMEOUT_SECONDS) plus one retry
on the ChatOpenAI and LLMClient clients. Context is preserved because
conversation history is rebuilt from Postgres on every request.
2026-09-11 17:05:10 -04:00
ryan ce461b6ff9 Merge pull request 'Fix missing React import in AnswerBubble' (#42) from fix-answer-bubble-react-import into main
Reviewed-on: #42
2026-09-11 16:41:15 -04:00
4 changed files with 47 additions and 1 deletions
+10
View File
@@ -124,6 +124,16 @@ make test # Run tests
make test-cov # Run tests with coverage
```
Note: `make test` may fail locally (no pytest on PATH); use `.venv/bin/pytest tests/ -q` instead.
## Git Workflow
- The host runs **Gitea** (`git.torrtle.co`), not GitHub, despite the remote name. Use the **`tea` CLI** for PRs (Gitea flavor: `tea pr create`, `tea pr list`, etc. — no `tea create`).
- Typical flow: commit locally, `git push -u origin <branch>`, then `tea pr create -t "title" -d "body"`.
- "kermit" is the user's slang for **commit** (e.g. "kermit and push" = commit and push). There is no kermit tool.
- Keep one logical change per branch/PR; don't ride along on stale feature branches.
- Don't commit `.DS_Store`, `chroma_db/`, `chromadb/`, `.planning/` (local artifacts).
## Key Patterns
- All endpoints are async (`async def`)
+10
View File
@@ -1,6 +1,7 @@
import os
from typing import cast
import httpx
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import BaseChatModel
@@ -20,12 +21,21 @@ from utils.ynab_service import YNABService
load_dotenv()
# Configure LLM with llama-server or OpenAI fallback
# Read timeout between streamed chunks. The OpenAI client defaults to 600s,
# which means a wedged llama-server blocks a request for 10 minutes before
# erroring. With a tight timeout, a hang raises quickly so
# with_fallbacks() below kicks in and the conversation (rebuilt from the
# DB each request) continues seamlessly on OpenAI.
LLAMA_READ_TIMEOUT = float(os.getenv("LLAMA_TIMEOUT_SECONDS", "120"))
llama_url = os.getenv("LLAMA_SERVER_URL")
if llama_url:
llama_chat = ChatOpenAI(
base_url=llama_url,
api_key="not-needed",
model=os.getenv("LLAMA_MODEL_NAME", "llama-3.1-8b-instruct"),
timeout=httpx.Timeout(LLAMA_READ_TIMEOUT, connect=10.0),
max_retries=1,
)
else:
llama_chat = None
+10 -1
View File
@@ -1,6 +1,7 @@
import os
import logging
import httpx
from openai import OpenAI
from dotenv import load_dotenv
@@ -13,7 +14,15 @@ class LLMClient:
def __init__(self):
llama_url = os.getenv("LLAMA_SERVER_URL")
if llama_url:
self.client = OpenAI(base_url=llama_url, api_key="not-needed")
# Tight read timeout so a wedged llama-server fails fast instead
# of hanging for the OpenAI client's 600s default.
read_timeout = float(os.getenv("LLAMA_TIMEOUT_SECONDS", "120"))
self.client = OpenAI(
base_url=llama_url,
api_key="not-needed",
timeout=httpx.Timeout(read_timeout, connect=10.0),
max_retries=1,
)
self.model = os.getenv("LLAMA_MODEL_NAME", "llama-3.1-8b-instruct")
self.PROVIDER = "llama_server"
logging.info("Using llama_server as LLM backend")
@@ -65,6 +65,22 @@ export const MessageInput = React.memo(
e.target.value = "";
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
if (isLoading) return;
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
e.preventDefault();
onImageSelect(file);
return;
}
}
}
};
const canSend = !isLoading && Boolean(query.trim() || pendingImage);
return (
@@ -100,6 +116,7 @@ export const MessageInput = React.memo(
ref={textareaRef}
onChange={handleQueryChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
value={query}
rows={2}
placeholder="Ask Simba anything..."