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:
log.error("cannot subscribe — no AMQP channel")
return
# Track subscription before attempting so reconnect catches it even if this try fails
_subscriptions.append((exchange, routing_keys, handler))
try:
queue = await ch.declare_queue("", exclusive=True)
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)
await queue.consume(_dispatch)
_subscriptions.append((exchange, routing_keys, handler))
except Exception:
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)
syslog_address = os.environ.get("CAIC_SYSLOG_ADDRESS", "/dev/log")
if syslog_address:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_address)
syslog_handler.setFormatter(logging.Formatter("caic[%(process)d]: %(levelname)s %(message)s"))
log.addHandler(syslog_handler)
try:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_address)
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
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
+6 -6
View File
@@ -9,9 +9,9 @@ import logging
log = logging.getLogger("caic")
VERSION = "v0.22.0"
VERSION = "v0.22.1"
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")
DEFAULT_MODEL = "qwen2.5-7b-instruct"
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_EXCHANGE_ADMIN = "jc.admin"
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:
url = os.environ.get("CAIC_AMQP_URL")
@@ -33,7 +33,7 @@ def get_amqp_url() -> str:
except (FileNotFoundError, OSError):
pw = "password"
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 ---
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")
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"}
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")
UPLOAD_CONTEXT_EXPIRY_HOURS = 1
BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES
# --- 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_LOW_WATER = 0.20
RAG_EVICTION_BATCH = 1000
+1
View File
@@ -29,6 +29,7 @@ def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL")
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")
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")
RAG_SCORE_THRESHOLD = 0.25
+21 -19
View File
@@ -120,26 +120,28 @@ async def chat_completions(request: Request):
# --- Persist conversation ---
db = get_db()
now = datetime.now(timezone.utc).isoformat()
conv_id = str(uuid.uuid4())
title = f"[IDE] {user_message[:72]}{'...' if len(user_message) > 72 else ''}"
db.execute(
"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")
content = msg.get("content", "")
if role in ("user", "assistant"):
db.execute(
"INSERT INTO messages (conversation_id, role, content, created_at, perplexity) VALUES (?, ?, ?, ?, ?)",
(conv_id, role, encrypt_text(content), now, None),
)
db.commit()
try:
now = datetime.now(timezone.utc).isoformat()
conv_id = str(uuid.uuid4())
title = f"[IDE] {user_message[:72]}{'...' if len(user_message) > 72 else ''}"
db.execute(
"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")
content = msg.get("content", "")
if role in ("user", "assistant"):
db.execute(
"INSERT INTO messages (conversation_id, role, content, created_at, perplexity) VALUES (?, ?, ?, ?, ?)",
(conv_id, role, encrypt_text(content), now, None),
)
db.commit()
# --- Build system prompt through full jC pipeline ---
system_prompt = await build_system_prompt(db, "", user_message)
db.close()
# --- Build system prompt through full jC pipeline ---
system_prompt = await build_system_prompt(db, "", user_message)
finally:
db.close()
# Assemble messages for upstream: inject jC system prompt, preserve history
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))