mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 04:17:56 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
125 lines
4.9 KiB
Python
125 lines
4.9 KiB
Python
"""Repository for the ``stack_logs`` table.
|
|
|
|
Covers the single operation the legacy Mongo code performs:
|
|
|
|
1. ``insert_one`` in logging.py ``_log_to_mongodb`` — append-only debug/error
|
|
activity log. The Mongo collection is ``stack_logs``; the Mongo variable
|
|
inside ``_log_to_mongodb`` is misleadingly named ``user_logs_collection``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from docsgpt.storage.db.redaction import redact_secrets
|
|
from docsgpt.storage.db.serialization import PGNativeJSONEncoder
|
|
from docsgpt.utils import strip_null_bytes
|
|
|
|
from sqlalchemy import Connection, text
|
|
|
|
# Longest string kept inside ``stacks``. The scalar columns are truncated
|
|
# by the caller, but stacks used to go in whole — one uncapped tool
|
|
# result (634k tokens, 07-17) rode into the activity log through here.
|
|
_STACKS_STRING_MAX_LEN = 10000
|
|
|
|
|
|
def _bound_strings(value):
|
|
"""Recursively truncate strings in ``value`` to ``_STACKS_STRING_MAX_LEN``."""
|
|
if isinstance(value, str):
|
|
if len(value) <= _STACKS_STRING_MAX_LEN:
|
|
return value
|
|
return value[:_STACKS_STRING_MAX_LEN] + "...[truncated]"
|
|
if isinstance(value, dict):
|
|
return {k: _bound_strings(v) for k, v in value.items()}
|
|
if isinstance(value, (list, tuple)):
|
|
return [_bound_strings(item) for item in value]
|
|
return value
|
|
|
|
|
|
class StackLogsRepository:
|
|
"""Postgres-backed replacement for Mongo ``stack_logs`` collection."""
|
|
|
|
def __init__(self, conn: Connection) -> None:
|
|
self._conn = conn
|
|
|
|
def insert(
|
|
self,
|
|
*,
|
|
activity_id: str,
|
|
endpoint: Optional[str] = None,
|
|
level: Optional[str] = None,
|
|
user_id: Optional[str] = None,
|
|
api_key: Optional[str] = None,
|
|
agent_id: Optional[str] = None,
|
|
query: Optional[str] = None,
|
|
stacks: Optional[list] = None,
|
|
timestamp: Optional[datetime] = None,
|
|
) -> None:
|
|
# ``agent_id`` is a UUID column. Parse it with ``uuid.UUID`` and coerce
|
|
# anything that isn't a valid UUID (e.g. a 24-hex legacy Mongo ObjectId)
|
|
# to NULL so a stray/legacy id never reaches ``CAST(... AS uuid)`` and
|
|
# breaks the activity-log write.
|
|
agent_id_uuid: Optional[str] = None
|
|
if agent_id:
|
|
try:
|
|
agent_id_uuid = str(uuid.UUID(str(agent_id)))
|
|
except (ValueError, AttributeError, TypeError):
|
|
agent_id_uuid = None
|
|
self._conn.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO stack_logs (activity_id, endpoint, level, user_id, api_key, agent_id, query, stacks, timestamp)
|
|
VALUES (
|
|
:activity_id, :endpoint, :level, :user_id, :api_key,
|
|
CAST(:agent_id AS uuid), :query,
|
|
CAST(:stacks AS jsonb),
|
|
COALESCE(:timestamp, now())
|
|
)
|
|
"""
|
|
),
|
|
{
|
|
"activity_id": activity_id,
|
|
"endpoint": strip_null_bytes(endpoint),
|
|
"level": strip_null_bytes(level),
|
|
"user_id": strip_null_bytes(user_id),
|
|
"api_key": strip_null_bytes(api_key),
|
|
"agent_id": agent_id_uuid,
|
|
"query": strip_null_bytes(query),
|
|
"stacks": json.dumps(
|
|
redact_secrets(
|
|
_bound_strings(strip_null_bytes(stacks or []))
|
|
),
|
|
cls=PGNativeJSONEncoder,
|
|
),
|
|
"timestamp": timestamp,
|
|
},
|
|
)
|
|
|
|
def reassign_api_key(self, *, old_key: str, new_key: str) -> int:
|
|
"""Re-point historical rows from ``old_key`` to ``new_key``.
|
|
|
|
Since migration 0026 ``stack_logs`` has an ``agent_id`` column and the
|
|
analytics ``webhook_where`` / ``system_where`` clauses match
|
|
``agent_id`` first with ``api_key`` as a fallback, so most rows already
|
|
survive a key rotation via ``agent_id``. This rewrite still runs to keep
|
|
the ``api_key`` column consistent and to re-attach any rows whose
|
|
``agent_id`` is NULL (e.g. a log written without agent context).
|
|
|
|
Matched by ``api_key`` only — deliberately NOT scoped by ``user_id``:
|
|
``agents.key`` is globally unique, so a key maps to exactly one agent,
|
|
and rows are stamped with the *caller's* user_id (which is not the
|
|
owner for webhook / external-api-key traffic). Scoping by owner would
|
|
skip precisely the rows this rewrite exists to preserve.
|
|
Returns the number of rows updated.
|
|
"""
|
|
if not old_key or not new_key:
|
|
return 0
|
|
result = self._conn.execute(
|
|
text("UPDATE stack_logs SET api_key = :new_key WHERE api_key = :old_key"),
|
|
{"old_key": old_key, "new_key": new_key},
|
|
)
|
|
return result.rowcount
|