diff --git a/amqp.py b/amqp.py index 3a307a7..818f119 100644 --- a/amqp.py +++ b/amqp.py @@ -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) diff --git a/app.py b/app.py index bb1dfec..af323e5 100644 --- a/app.py +++ b/app.py @@ -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")) diff --git a/config.py b/config.py index 559cd92..df3abb0 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/db.py b/db.py index 40cc0f2..cad2253 100644 --- a/db.py +++ b/db.py @@ -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 diff --git a/rag.py b/rag.py index 42b2e6f..5532824 100644 --- a/rag.py +++ b/rag.py @@ -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 diff --git a/routers/completions.py b/routers/completions.py index 8c197c4..875ee90 100644 --- a/routers/completions.py +++ b/routers/completions.py @@ -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 = [] diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..ab41d02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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))