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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 = []
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user