v0.22.1: dockerize defaults, harden error handling, fix test discovery

- config.py: defaults to localhost/localhost, AMQP to rabbitmq, secret
  path to /run/secrets, RAG_MAX_VECTORS from env
- rag.py: EMBED_URL default to localhost
- app.py: syslog handler wrapped in try/except
- routers/completions.py: db.close() in try/finally
- db.py: PRAGMA journal_mode = WAL
- amqp.py: subscription append before try for reconnect safety
- tests/conftest.py: add project root to sys.path for test discovery
This commit is contained in:
gramps
2026-07-19 16:16:40 -07:00
parent 666a237a8b
commit 54cca366a4
7 changed files with 43 additions and 30 deletions
+2 -1
View File
@@ -43,6 +43,8 @@ async def subscribe(exchange: str, routing_keys: list[str], handler) -> None:
if ch is None: if ch is None:
log.error("cannot subscribe — no AMQP channel") log.error("cannot subscribe — no AMQP channel")
return return
# Track subscription before attempting so reconnect catches it even if this try fails
_subscriptions.append((exchange, routing_keys, handler))
try: try:
queue = await ch.declare_queue("", exclusive=True) queue = await ch.declare_queue("", exclusive=True)
ex = await ch.get_exchange(exchange) ex = await ch.get_exchange(exchange)
@@ -58,7 +60,6 @@ async def subscribe(exchange: str, routing_keys: list[str], handler) -> None:
log.exception("AMQP handler error for %s %s", exchange, msg.routing_key) log.exception("AMQP handler error for %s %s", exchange, msg.routing_key)
await queue.consume(_dispatch) await queue.consume(_dispatch)
_subscriptions.append((exchange, routing_keys, handler))
except Exception: except Exception:
log.exception("AMQP subscribe failed for %s %s", exchange, routing_keys) log.exception("AMQP subscribe failed for %s %s", exchange, routing_keys)
+6 -3
View File
@@ -47,9 +47,12 @@ log = logging.getLogger("caic")
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
syslog_address = os.environ.get("CAIC_SYSLOG_ADDRESS", "/dev/log") syslog_address = os.environ.get("CAIC_SYSLOG_ADDRESS", "/dev/log")
if syslog_address: if syslog_address:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_address) try:
syslog_handler.setFormatter(logging.Formatter("caic[%(process)d]: %(levelname)s %(message)s")) syslog_handler = logging.handlers.SysLogHandler(address=syslog_address)
log.addHandler(syslog_handler) syslog_handler.setFormatter(logging.Formatter("caic[%(process)d]: %(levelname)s %(message)s"))
log.addHandler(syslog_handler)
except Exception:
log.warning("syslog not available at %s -- skipping", syslog_address)
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
+6 -6
View File
@@ -9,9 +9,9 @@ import logging
log = logging.getLogger("caic") log = logging.getLogger("caic")
VERSION = "v0.22.0" VERSION = "v0.22.1"
OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434") OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434")
LLAMA_SERVER_BASE = os.environ.get("LLAMA_SERVER_BASE", "http://192.168.50.108:8081") LLAMA_SERVER_BASE = os.environ.get("LLAMA_SERVER_BASE", "http://localhost:8081")
SEARXNG_BASE = os.environ.get("CAIC_SEARXNG_BASE", "http://localhost:8888") SEARXNG_BASE = os.environ.get("CAIC_SEARXNG_BASE", "http://localhost:8888")
DEFAULT_MODEL = "qwen2.5-7b-instruct" DEFAULT_MODEL = "qwen2.5-7b-instruct"
COMPLETIONS_API_KEY = os.environ.get("CAIC_COMPLETIONS_API_KEY", "caic-sk-" + os.urandom(24).hex()) COMPLETIONS_API_KEY = os.environ.get("CAIC_COMPLETIONS_API_KEY", "caic-sk-" + os.urandom(24).hex())
@@ -21,7 +21,7 @@ MODEL_CONTEXT_LENGTH = 4096
AMQP_RECONNECT_DELAY = 5 AMQP_RECONNECT_DELAY = 5
AMQP_EXCHANGE_ADMIN = "jc.admin" AMQP_EXCHANGE_ADMIN = "jc.admin"
AMQP_EXCHANGE_SYSTEM = "jc.system" AMQP_EXCHANGE_SYSTEM = "jc.system"
AMQP_SECRET_PATH = os.environ.get("CAIC_AMQP_SECRET_PATH", "/home/gramps/.caic_amqp_secret") AMQP_SECRET_PATH = os.environ.get("CAIC_AMQP_SECRET_PATH", "/run/secrets/caic_amqp_secret")
def get_amqp_url() -> str: def get_amqp_url() -> str:
url = os.environ.get("CAIC_AMQP_URL") url = os.environ.get("CAIC_AMQP_URL")
@@ -33,7 +33,7 @@ def get_amqp_url() -> str:
except (FileNotFoundError, OSError): except (FileNotFoundError, OSError):
pw = "password" pw = "password"
log.warning("AMQP secret file not found at %s — using default password", AMQP_SECRET_PATH) log.warning("AMQP secret file not found at %s — using default password", AMQP_SECRET_PATH)
return f"amqp://caic:{pw}@localhost:5672/caic" return f"amqp://caic:{pw}@rabbitmq:5672/caic"
# --- Auth --- # --- Auth ---
SESSION_TIMEOUT_SECONDS = 3600 SESSION_TIMEOUT_SECONDS = 3600
@@ -69,13 +69,13 @@ BODY_LIMIT_PROFILE_BYTES = 256 * 1024
UPLOAD_DIR = os.environ.get("CAIC_UPLOAD_DIR", "/tmp/caic_uploads") UPLOAD_DIR = os.environ.get("CAIC_UPLOAD_DIR", "/tmp/caic_uploads")
MAX_UPLOAD_BYTES = 20 * 1024 * 1024 MAX_UPLOAD_BYTES = 20 * 1024 * 1024
SUPPORTED_UPLOAD_TYPES = {"text/plain", "text/markdown", "application/pdf", "application/json", "text/x-python", "text/html", "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"} SUPPORTED_UPLOAD_TYPES = {"text/plain", "text/markdown", "application/pdf", "application/json", "text/x-python", "text/html", "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"}
QDRANT_URL = os.environ.get("CAIC_QDRANT_URL", "http://192.168.50.108:6333") QDRANT_URL = os.environ.get("CAIC_QDRANT_URL", "http://localhost:6333")
RAG_COLLECTION = os.environ.get("CAIC_RAG_COLLECTION", "caic_rag") RAG_COLLECTION = os.environ.get("CAIC_RAG_COLLECTION", "caic_rag")
UPLOAD_CONTEXT_EXPIRY_HOURS = 1 UPLOAD_CONTEXT_EXPIRY_HOURS = 1
BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES
# --- RAG eviction --- # --- RAG eviction ---
RAG_MAX_VECTORS = 50000 RAG_MAX_VECTORS = int(os.environ.get("CAIC_RAG_MAX_VECTORS", "50000"))
RAG_EVICTION_HIGH_WATER = 0.80 RAG_EVICTION_HIGH_WATER = 0.80
RAG_EVICTION_LOW_WATER = 0.20 RAG_EVICTION_LOW_WATER = 0.20
RAG_EVICTION_BATCH = 1000 RAG_EVICTION_BATCH = 1000
+1
View File
@@ -29,6 +29,7 @@ def get_db():
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL")
return conn return conn
+1 -1
View File
@@ -16,7 +16,7 @@ from config import MAX_SKILL_PROMPT_CHARS, QDRANT_URL, RAG_COLLECTION
log = logging.getLogger("caic") log = logging.getLogger("caic")
EMBED_URL = os.environ.get("CAIC_EMBED_URL", "http://192.168.50.210:11434") EMBED_URL = os.environ.get("CAIC_EMBED_URL", "http://localhost:11434")
EMBED_MODEL = os.environ.get("CAIC_EMBED_MODEL", "mxbai-embed-large") EMBED_MODEL = os.environ.get("CAIC_EMBED_MODEL", "mxbai-embed-large")
RAG_SCORE_THRESHOLD = 0.25 RAG_SCORE_THRESHOLD = 0.25
+21 -19
View File
@@ -120,26 +120,28 @@ async def chat_completions(request: Request):
# --- Persist conversation --- # --- Persist conversation ---
db = get_db() db = get_db()
now = datetime.now(timezone.utc).isoformat() try:
conv_id = str(uuid.uuid4()) now = datetime.now(timezone.utc).isoformat()
title = f"[IDE] {user_message[:72]}{'...' if len(user_message) > 72 else ''}" conv_id = str(uuid.uuid4())
db.execute( title = f"[IDE] {user_message[:72]}{'...' if len(user_message) > 72 else ''}"
"INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", db.execute(
(conv_id, encrypt_text(title), model, now, now), "INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
) (conv_id, encrypt_text(title), model, now, now),
for msg in messages: )
role = msg.get("role") for msg in messages:
content = msg.get("content", "") role = msg.get("role")
if role in ("user", "assistant"): content = msg.get("content", "")
db.execute( if role in ("user", "assistant"):
"INSERT INTO messages (conversation_id, role, content, created_at, perplexity) VALUES (?, ?, ?, ?, ?)", db.execute(
(conv_id, role, encrypt_text(content), now, None), "INSERT INTO messages (conversation_id, role, content, created_at, perplexity) VALUES (?, ?, ?, ?, ?)",
) (conv_id, role, encrypt_text(content), now, None),
db.commit() )
db.commit()
# --- Build system prompt through full jC pipeline --- # --- Build system prompt through full jC pipeline ---
system_prompt = await build_system_prompt(db, "", user_message) system_prompt = await build_system_prompt(db, "", user_message)
db.close() finally:
db.close()
# Assemble messages for upstream: inject jC system prompt, preserve history # Assemble messages for upstream: inject jC system prompt, preserve history
upstream_messages = [] upstream_messages = []
+6
View File
@@ -0,0 +1,6 @@
# Ensure the project root is on sys.path so that test modules can import
# top-level packages (app, amqp, cluster, config, …) without PYTHONPATH hacks.
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))