token-optimise 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +0 -0
- src/core/__init__.py +0 -0
- src/core/cache.py +126 -0
- src/core/client.py +114 -0
- src/core/db.py +299 -0
- src/core/document_search.py +180 -0
- src/core/tool_selection.py +119 -0
- src/core/trim.py +55 -0
- src/front.py +477 -0
- src/mcp/__init__.py +0 -0
- src/mcp/server.py +866 -0
- src/mcp/server_http.py +171 -0
- token_optimise/__init__.py +0 -0
- token_optimise/__main__.py +272 -0
- token_optimise/config.py +32 -0
- token_optimise-0.1.0.dist-info/METADATA +498 -0
- token_optimise-0.1.0.dist-info/RECORD +19 -0
- token_optimise-0.1.0.dist-info/WHEEL +4 -0
- token_optimise-0.1.0.dist-info/entry_points.txt +2 -0
src/__init__.py
ADDED
|
File without changes
|
src/core/__init__.py
ADDED
|
File without changes
|
src/core/cache.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import hashlib, time, os, logging
|
|
2
|
+
from src.core.client import _chroma_client, CHROMA_PATH
|
|
3
|
+
from chromadb.utils import embedding_functions
|
|
4
|
+
from token_optimise.config import settings, PROJECT_ROOT
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("token")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TTL_BY_TOOL = {
|
|
10
|
+
"execute": 300,
|
|
11
|
+
"ask_document": 86400,
|
|
12
|
+
"search_all_documents": 86400,
|
|
13
|
+
"list_indexed_documents": 60,
|
|
14
|
+
"index_document": 0,
|
|
15
|
+
"index_documents_folder": 0,
|
|
16
|
+
}
|
|
17
|
+
TTL_DEFAULT = 3600
|
|
18
|
+
|
|
19
|
+
_cache_collection = None
|
|
20
|
+
|
|
21
|
+
def _get_cache():
|
|
22
|
+
global _cache_collection
|
|
23
|
+
if _cache_collection is None:
|
|
24
|
+
embedder = embedding_functions.SentenceTransformerEmbeddingFunction(
|
|
25
|
+
model_name=settings.embedder
|
|
26
|
+
)
|
|
27
|
+
_cache_collection = _chroma_client.get_or_create_collection(
|
|
28
|
+
name="semantic_cache", embedding_function=embedder
|
|
29
|
+
)
|
|
30
|
+
return _cache_collection
|
|
31
|
+
|
|
32
|
+
def check_cache(query: str, tool_name: str = ""):
|
|
33
|
+
"""Returns (answer, similarity). answer is None on miss or expiry."""
|
|
34
|
+
try:
|
|
35
|
+
results = _get_cache().query(query_texts=[query], n_results=1)
|
|
36
|
+
if not results["documents"][0]:
|
|
37
|
+
return None, 0.0
|
|
38
|
+
distance = results["distances"][0][0]
|
|
39
|
+
similarity = 1 - distance
|
|
40
|
+
if similarity >= 0.8:
|
|
41
|
+
meta = results["metadatas"][0][0]
|
|
42
|
+
cached_at = meta.get("cached_at", 0)
|
|
43
|
+
ttl = TTL_BY_TOOL.get(tool_name, TTL_DEFAULT)
|
|
44
|
+
if ttl == 0:
|
|
45
|
+
return None, similarity
|
|
46
|
+
age = time.time() - cached_at
|
|
47
|
+
if age > ttl:
|
|
48
|
+
logger.info(
|
|
49
|
+
f"[CACHE] EXPIRED for '{tool_name}' | age={int(age)}s ttl={ttl}s"
|
|
50
|
+
)
|
|
51
|
+
return None, similarity
|
|
52
|
+
logger.info(f"[CACHE] HIT for '{tool_name}' | sim={similarity:.3f} age={int(age)}s")
|
|
53
|
+
return meta["answer"], similarity
|
|
54
|
+
return None, similarity
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.warning(f"[CACHE] check failed: {e}", exc_info=True)
|
|
57
|
+
return None, 0.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_cache_exact(key: str, tool_name: str = ""):
|
|
61
|
+
try:
|
|
62
|
+
result = _get_cache().get(ids=[key], include=["metadatas"])
|
|
63
|
+
if not result["ids"]:
|
|
64
|
+
return None, 0.0
|
|
65
|
+
meta = result["metadatas"][0]
|
|
66
|
+
cached_at = meta.get("cached_at", 0)
|
|
67
|
+
ttl = TTL_BY_TOOL.get(tool_name, TTL_DEFAULT)
|
|
68
|
+
if ttl == 0:
|
|
69
|
+
return None, 1.0
|
|
70
|
+
age = time.time() - cached_at
|
|
71
|
+
if age > ttl:
|
|
72
|
+
logger.info(f"[CACHE] EXPIRED (exact) for '{tool_name}' | age={int(age)}s")
|
|
73
|
+
return None, 1.0
|
|
74
|
+
logger.info(f"[CACHE] HIT (exact) for '{tool_name}'")
|
|
75
|
+
return meta["answer"], 1.0
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"[CACHE] exact check failed: {e}")
|
|
78
|
+
return None, 0.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def store_answer(query, answer,tool_name):
|
|
84
|
+
query_id = hashlib.sha256(query.strip().lower().encode()).hexdigest()
|
|
85
|
+
try:
|
|
86
|
+
_get_cache().upsert(
|
|
87
|
+
ids=[query_id],
|
|
88
|
+
documents=[query],
|
|
89
|
+
metadatas=[{"answer": answer, "cached_at": time.time(), "tool_name": tool_name}],
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"[CACHE] store failed: {e}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def store_answer_exact(key: str, answer: str, tool_name: str) -> None:
|
|
96
|
+
"""Store by exact key (no embedding). Used for doc queries."""
|
|
97
|
+
try:
|
|
98
|
+
_get_cache().upsert(
|
|
99
|
+
ids=[key],
|
|
100
|
+
documents=[key],
|
|
101
|
+
metadatas=[{"answer": answer, "cached_at": time.time(), "tool_name": tool_name}],
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.warning(f"[CACHE] exact store failed: {e}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cleanup_cache():
|
|
109
|
+
try:
|
|
110
|
+
all_entries = _get_cache().get(include=["metadatas"])
|
|
111
|
+
now = time.time()
|
|
112
|
+
expired_ids = []
|
|
113
|
+
for id_, meta in zip(all_entries["ids"], all_entries["metadatas"]):
|
|
114
|
+
tool_name = meta.get("tool_name", "")
|
|
115
|
+
ttl = TTL_BY_TOOL.get(tool_name, TTL_DEFAULT)
|
|
116
|
+
if ttl == 0:
|
|
117
|
+
continue
|
|
118
|
+
if now - meta.get("cached_at", 0) > ttl:
|
|
119
|
+
expired_ids.append(id_)
|
|
120
|
+
if expired_ids:
|
|
121
|
+
_get_cache().delete(ids=expired_ids)
|
|
122
|
+
logger.info(f"[CACHE] cleaned up {len(expired_ids)} expired entries")
|
|
123
|
+
return len(expired_ids)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.warning(f"[CACHE] cleanup failed: {e}")
|
|
126
|
+
return 0
|
src/core/client.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from groq import Groq
|
|
2
|
+
from token_optimise.config import settings,PROJECT_ROOT
|
|
3
|
+
import chromadb
|
|
4
|
+
import os,json
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("token")
|
|
8
|
+
|
|
9
|
+
_groq_client = Groq(api_key=settings.groq_api_key)
|
|
10
|
+
CHROMA_PATH = os.path.join(PROJECT_ROOT, "storage", "chroma_db")
|
|
11
|
+
_chroma_client = chromadb.PersistentClient(path=CHROMA_PATH)
|
|
12
|
+
|
|
13
|
+
def fill_args_llm(query,schema):
|
|
14
|
+
props = schema.get("properties", {})
|
|
15
|
+
required = schema.get("required", [])
|
|
16
|
+
schema_summary="\n".join([
|
|
17
|
+
f"- {name} ({info.get('type','string')}) : {info.get('description','no desc')}"
|
|
18
|
+
for name,info in props.items()
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
prompt = (
|
|
22
|
+
f"You are a tool argument filler. Given a tool schema and a user query, "
|
|
23
|
+
f"return ONLY a valid JSON object with the correct arguments.\n"
|
|
24
|
+
f"Tool parameters:\n{schema_summary}\n"
|
|
25
|
+
f"Required fields: {required}\n"
|
|
26
|
+
f"User query: \"{query}\"\n\n"
|
|
27
|
+
f"Rules:\n"
|
|
28
|
+
f"- Return ONLY a JSON object, no explanation, no markdown, no backticks\n"
|
|
29
|
+
f"- For file paths: NEVER use ~ or relative paths. Always expand to full absolute path.\n"
|
|
30
|
+
f"- Home directory is: {os.path.expanduser('~')}\n"
|
|
31
|
+
f"- Desktop is: {os.path.expanduser('~/Desktop')}\n"
|
|
32
|
+
f"- Documents is: {os.path.expanduser('~/Documents')}\n"
|
|
33
|
+
f"JSON:"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
raw = _groq_client.chat.completions.create(
|
|
39
|
+
model=settings.groq_model,
|
|
40
|
+
messages=[{"role": "user", "content": prompt}],
|
|
41
|
+
temperature=0,
|
|
42
|
+
max_tokens=settings.max_response_tokens,
|
|
43
|
+
)
|
|
44
|
+
usage = raw.usage
|
|
45
|
+
logger.info(
|
|
46
|
+
f"[GROQ] fill_args | prompt_tokens={usage.prompt_tokens} "
|
|
47
|
+
f"completion_tokens={usage.completion_tokens}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
response = raw.choices[0].message.content.strip()
|
|
51
|
+
|
|
52
|
+
if response.startswith("```"):
|
|
53
|
+
response = response.split("```")[1]
|
|
54
|
+
if response.startswith("json"):
|
|
55
|
+
response = response[4:]
|
|
56
|
+
response = response.strip()
|
|
57
|
+
|
|
58
|
+
args = json.loads(response)
|
|
59
|
+
result = {k: v for k, v in args.items() if v is not None}
|
|
60
|
+
result["_groq_usage"] = {
|
|
61
|
+
"prompt_tokens": usage.prompt_tokens,
|
|
62
|
+
"completion_tokens": usage.completion_tokens,
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
except json.JSONDecodeError as e:
|
|
67
|
+
logger.warning(f"[GROQ] failed to parse args JSON: {e}")
|
|
68
|
+
return {"_groq_error": f"JSONDecodeError: {e}"}
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"[GROQ] fill_args_llm failed: {e}")
|
|
71
|
+
return {"_groq_error": str(e)}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def expand_query(query: str, tool_descriptions: list[dict]) -> tuple[str, int, int]:
|
|
75
|
+
"""Rewrite query using tool terminology. Returns (rewritten, prompt_tokens, completion_tokens)."""
|
|
76
|
+
if not tool_descriptions:
|
|
77
|
+
return query, 0, 0
|
|
78
|
+
|
|
79
|
+
# Using all the tools, capping stopped.
|
|
80
|
+
tools_text = "\n".join(
|
|
81
|
+
f"- {t['function']['name']}: {t['function'].get('description', '')}"
|
|
82
|
+
for t in tool_descriptions
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
resp = _groq_client.chat.completions.create(
|
|
87
|
+
model=settings.groq_model,
|
|
88
|
+
messages=[
|
|
89
|
+
{
|
|
90
|
+
"role": "system",
|
|
91
|
+
"content": (
|
|
92
|
+
"You are a query rewriter. Given a user query and a list of available tools, "
|
|
93
|
+
"rewrite the query using the exact terminology and phrasing that best matches "
|
|
94
|
+
"the tool descriptions. Output ONLY the rewritten query, nothing else."
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"role": "user",
|
|
99
|
+
"content": f"Tools:\n{tools_text}\n\nUser query: {query}\n\nRewritten query:",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
max_tokens=60,
|
|
103
|
+
temperature=0.0,
|
|
104
|
+
)
|
|
105
|
+
usage = resp.usage
|
|
106
|
+
logger.info(
|
|
107
|
+
f"[GROQ] expand_query | prompt_tokens={usage.prompt_tokens} "
|
|
108
|
+
f"completion_tokens={usage.completion_tokens}"
|
|
109
|
+
)
|
|
110
|
+
rewritten = resp.choices[0].message.content.strip()
|
|
111
|
+
return (rewritten if rewritten else query, usage.prompt_tokens, usage.completion_tokens)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f"[EXPAND_QUERY] failed: {e}")
|
|
114
|
+
return query, 0, 0
|
src/core/db.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import sqlite3
|
|
7
|
+
import requests
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from token_optimise.config import PROJECT_ROOT,settings
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("token")
|
|
12
|
+
|
|
13
|
+
DB_PATH = os.path.join(PROJECT_ROOT, "storage", "token_events.db")
|
|
14
|
+
|
|
15
|
+
INGEST_URL = settings.token_ingest_url
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONVERSATION_TIMEOUT_MINUTES = 15
|
|
19
|
+
_current_conversation_id = None
|
|
20
|
+
_last_event_time = None
|
|
21
|
+
_conv_lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_or_create_conversation_id() -> str:
|
|
25
|
+
global _current_conversation_id, _last_event_time
|
|
26
|
+
with _conv_lock:
|
|
27
|
+
now = time.time()
|
|
28
|
+
if (
|
|
29
|
+
_current_conversation_id is None
|
|
30
|
+
or _last_event_time is None
|
|
31
|
+
or (now - _last_event_time) > CONVERSATION_TIMEOUT_MINUTES * 60
|
|
32
|
+
):
|
|
33
|
+
_current_conversation_id = str(uuid.uuid4())
|
|
34
|
+
_last_event_time = now
|
|
35
|
+
return _current_conversation_id
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_connection() -> sqlite3.Connection:
|
|
39
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
40
|
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
41
|
+
conn.row_factory = sqlite3.Row
|
|
42
|
+
return conn
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def get_db():
|
|
47
|
+
conn = get_connection()
|
|
48
|
+
try:
|
|
49
|
+
yield conn
|
|
50
|
+
finally:
|
|
51
|
+
conn.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def init_db(conn):
|
|
55
|
+
conn.execute("""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
user_id TEXT,
|
|
59
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
conversation_id TEXT,
|
|
61
|
+
tool_name TEXT NOT NULL,
|
|
62
|
+
query TEXT,
|
|
63
|
+
cache_hit INTEGER DEFAULT 0,
|
|
64
|
+
cache_similarity REAL DEFAULT 0.0,
|
|
65
|
+
tokens_before_trim INTEGER DEFAULT 0,
|
|
66
|
+
tokens_after_trim INTEGER DEFAULT 0,
|
|
67
|
+
trim_saved INTEGER DEFAULT 0,
|
|
68
|
+
schema_tokens_full INTEGER DEFAULT 0,
|
|
69
|
+
schema_tokens_selected INTEGER DEFAULT 0,
|
|
70
|
+
schema_tokens_saved INTEGER DEFAULT 0,
|
|
71
|
+
groq_prompt_tokens INTEGER DEFAULT 0,
|
|
72
|
+
groq_completion_tokens INTEGER DEFAULT 0,
|
|
73
|
+
doc_id TEXT,
|
|
74
|
+
success INTEGER DEFAULT 1
|
|
75
|
+
)
|
|
76
|
+
""")
|
|
77
|
+
for col in ["user_id", "conversation_id"]:
|
|
78
|
+
try:
|
|
79
|
+
conn.execute(f"ALTER TABLE events ADD COLUMN {col} TEXT")
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
conn.commit()
|
|
83
|
+
logger.info(f"[DB] SQLite ready at {DB_PATH}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def init_db_once() -> None:
|
|
88
|
+
"""Call once at server startup."""
|
|
89
|
+
with get_db() as conn:
|
|
90
|
+
init_db(conn)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _post_to_cloud(data: dict):
|
|
97
|
+
"""Send event to AWS /collect endpoint."""
|
|
98
|
+
if not INGEST_URL:
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
# Read token fresh — it may have been saved after module loaded
|
|
102
|
+
token = settings.collect_token or ""
|
|
103
|
+
headers = {"X-Collect-Token": token} if token else {}
|
|
104
|
+
requests.post(INGEST_URL, json=data, timeout=3, headers=headers)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.debug(f"[DB] Cloud ingest failed (local SQLite has it): {e}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def insert_event(**kwargs):
|
|
111
|
+
kwargs.setdefault("user_id", os.getenv("TOKEN_USER_ID", "unknown"))
|
|
112
|
+
kwargs.setdefault("conversation_id", _get_or_create_conversation_id())
|
|
113
|
+
|
|
114
|
+
fields = [
|
|
115
|
+
"user_id", "conversation_id",
|
|
116
|
+
"tool_name", "query", "cache_hit", "cache_similarity",
|
|
117
|
+
"tokens_before_trim", "tokens_after_trim", "trim_saved",
|
|
118
|
+
"schema_tokens_full", "schema_tokens_selected", "schema_tokens_saved",
|
|
119
|
+
"doc_id", "success",
|
|
120
|
+
"groq_prompt_tokens", "groq_completion_tokens",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
data = {f: kwargs.get(f, None) for f in fields}
|
|
124
|
+
|
|
125
|
+
# always write to local SQLite
|
|
126
|
+
with get_db() as conn:
|
|
127
|
+
placeholders = ", ".join(["?" for _ in fields])
|
|
128
|
+
columns = ", ".join(fields)
|
|
129
|
+
conn.execute(
|
|
130
|
+
f"INSERT INTO events ({columns}) VALUES ({placeholders})",
|
|
131
|
+
list(data.values()),
|
|
132
|
+
)
|
|
133
|
+
conn.commit()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_post_to_cloud(data)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _fetchall(conn, sql, params=()):
|
|
140
|
+
rows = conn.execute(sql, params).fetchall()
|
|
141
|
+
return [dict(r) for r in rows]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _fetchone(conn, sql, params=()):
|
|
146
|
+
row = conn.execute(sql, params).fetchone()
|
|
147
|
+
return dict(row) if row else {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_summary(conn):
|
|
151
|
+
return _fetchone(conn, """
|
|
152
|
+
SELECT
|
|
153
|
+
COUNT(*) as total_calls,
|
|
154
|
+
COALESCE(SUM(cache_hit), 0) as cache_hits,
|
|
155
|
+
COALESCE(ROUND(AVG(cache_hit) * 100, 1), 0.0) as hit_rate_pct,
|
|
156
|
+
COALESCE(SUM(trim_saved), 0) as total_trim_saved,
|
|
157
|
+
COALESCE(SUM(schema_tokens_saved), 0) as total_schema_saved,
|
|
158
|
+
COALESCE(SUM(trim_saved + schema_tokens_saved), 0) as total_tokens_saved
|
|
159
|
+
FROM events
|
|
160
|
+
""")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_recent_events(conn, limit=50):
|
|
165
|
+
return _fetchall(
|
|
166
|
+
conn, "SELECT * FROM events ORDER BY timestamp DESC LIMIT ?", (limit,)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def get_tool_stats(conn):
|
|
170
|
+
return _fetchall(conn,"""
|
|
171
|
+
SELECT
|
|
172
|
+
tool_name,
|
|
173
|
+
COUNT(*) AS calls,
|
|
174
|
+
SUM(cache_hit) AS hits,
|
|
175
|
+
SUM(trim_saved) AS trim_saved,
|
|
176
|
+
SUM(schema_tokens_saved) AS schema_saved
|
|
177
|
+
FROM events
|
|
178
|
+
GROUP BY tool_name
|
|
179
|
+
ORDER BY calls DESC
|
|
180
|
+
""")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_token_analysis(conn):
|
|
184
|
+
rows = _fetchone(conn, """
|
|
185
|
+
SELECT
|
|
186
|
+
COUNT(*) AS total_queries,
|
|
187
|
+
COALESCE(SUM(schema_tokens_saved), 0) AS schema_saved,
|
|
188
|
+
COALESCE(SUM(schema_tokens_full), 0) AS schema_full,
|
|
189
|
+
COALESCE(SUM(schema_tokens_selected), 0) AS schema_selected,
|
|
190
|
+
COALESCE(SUM(trim_saved), 0) AS trim_saved,
|
|
191
|
+
COALESCE(SUM(tokens_before_trim), 0) AS tokens_before_trim,
|
|
192
|
+
COALESCE(SUM(tokens_after_trim), 0) AS tokens_after_trim,
|
|
193
|
+
COALESCE(SUM(cache_hit), 0) AS cache_hits,
|
|
194
|
+
COUNT(CASE WHEN cache_hit = 0 THEN 1 END) as cache_misses
|
|
195
|
+
FROM events
|
|
196
|
+
""")
|
|
197
|
+
|
|
198
|
+
schema_saved = rows.get("schema_saved",0)
|
|
199
|
+
schema_full = rows.get("schema_full",0)
|
|
200
|
+
schema_selected = rows.get("schema_selected",0)
|
|
201
|
+
trim_saved = rows.get("trim_saved",0)
|
|
202
|
+
tokens_before = rows.get("tokens_before_trim",0)
|
|
203
|
+
tokens_after = rows.get("tokens_after_trim",0)
|
|
204
|
+
cache_hits = rows.get("cache_hits",0)
|
|
205
|
+
cache_misses = rows.get("cache_misses",0)
|
|
206
|
+
total_queries = rows.get("total_queries",0)
|
|
207
|
+
|
|
208
|
+
total_saved = schema_saved + trim_saved
|
|
209
|
+
|
|
210
|
+
actual_without= schema_full + tokens_before
|
|
211
|
+
actual_with = schema_selected + tokens_after
|
|
212
|
+
|
|
213
|
+
pct_saved = round((total_saved / actual_without * 100), 1) if actual_without > 0 else 0.0
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"total_queries": total_queries,
|
|
217
|
+
"cache_hits": cache_hits,
|
|
218
|
+
"cache_misses": cache_misses,
|
|
219
|
+
"schema_tokens_without_tom": schema_full,
|
|
220
|
+
"schema_tokens_with_tom": schema_selected,
|
|
221
|
+
"schema_saved": schema_saved,
|
|
222
|
+
"tokens_before_trim": tokens_before,
|
|
223
|
+
"tokens_after_trim": tokens_after,
|
|
224
|
+
"trim_saved": trim_saved,
|
|
225
|
+
"total_saved": total_saved,
|
|
226
|
+
"actual_without_tom": actual_without,
|
|
227
|
+
"actual_with_tom": actual_with,
|
|
228
|
+
"pct_saved": pct_saved,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def get_event_count(conn) -> int:
|
|
232
|
+
"""Quick row count for health check."""
|
|
233
|
+
row = _fetchone(conn, "SELECT COUNT(*) AS cnt FROM events")
|
|
234
|
+
return row.get("cnt", 0)
|
|
235
|
+
|
|
236
|
+
def get_conversation_stats(conn, limit=20):
|
|
237
|
+
return _fetchall(conn, """
|
|
238
|
+
SELECT
|
|
239
|
+
conversation_id,
|
|
240
|
+
MIN(timestamp) AS started_at,
|
|
241
|
+
COUNT(*) AS total_calls,
|
|
242
|
+
SUM(cache_hit) AS cache_hits,
|
|
243
|
+
SUM(trim_saved) AS trim_saved,
|
|
244
|
+
SUM(schema_tokens_saved) AS schema_saved,
|
|
245
|
+
SUM(trim_saved + schema_tokens_saved) AS total_saved
|
|
246
|
+
FROM events
|
|
247
|
+
WHERE conversation_id IS NOT NULL
|
|
248
|
+
GROUP BY conversation_id
|
|
249
|
+
ORDER BY started_at DESC
|
|
250
|
+
LIMIT ?
|
|
251
|
+
""", (limit,))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_groq_usage(conn):
|
|
256
|
+
return _fetchone(conn, """
|
|
257
|
+
SELECT
|
|
258
|
+
COALESCE(SUM(groq_prompt_tokens), 0) AS total_prompt_tokens,
|
|
259
|
+
COALESCE(SUM(groq_completion_tokens), 0) AS total_completion_tokens,
|
|
260
|
+
COALESCE(SUM(groq_prompt_tokens + groq_completion_tokens), 0) AS total_groq_tokens
|
|
261
|
+
FROM events
|
|
262
|
+
""")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_timeseries(conn, hours: int = 24):
|
|
266
|
+
return _fetchall(conn, """
|
|
267
|
+
SELECT
|
|
268
|
+
strftime('%Y-%m-%d %H:00', timestamp) AS hour,
|
|
269
|
+
COUNT(*) AS total_calls,
|
|
270
|
+
COALESCE(SUM(cache_hit), 0) AS cache_hits,
|
|
271
|
+
COALESCE(SUM(schema_tokens_saved), 0) AS schema_saved,
|
|
272
|
+
COALESCE(SUM(trim_saved), 0) AS trim_saved,
|
|
273
|
+
COALESCE(SUM(groq_prompt_tokens + groq_completion_tokens), 0) AS groq_tokens
|
|
274
|
+
FROM events
|
|
275
|
+
WHERE timestamp >= datetime('now', ? || ' hours')
|
|
276
|
+
GROUP BY hour
|
|
277
|
+
ORDER BY hour ASC
|
|
278
|
+
""", (f"-{hours}",))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_period_summary(conn, period: str = "today"):
|
|
283
|
+
_safe = {
|
|
284
|
+
"today": "date(timestamp) = date('now')",
|
|
285
|
+
"yesterday": "date(timestamp) = date('now', '-1 day')",
|
|
286
|
+
"week": "timestamp >= datetime('now', '-7 days')",
|
|
287
|
+
"all": "1=1",
|
|
288
|
+
}
|
|
289
|
+
where = _safe.get(period, "1=1")
|
|
290
|
+
return _fetchone(conn, f"""
|
|
291
|
+
SELECT
|
|
292
|
+
COUNT(*) AS total_calls,
|
|
293
|
+
COALESCE(SUM(cache_hit), 0) AS cache_hits,
|
|
294
|
+
COALESCE(SUM(schema_tokens_saved + trim_saved), 0) AS total_saved,
|
|
295
|
+
COALESCE(SUM(groq_prompt_tokens), 0) AS groq_prompt,
|
|
296
|
+
COALESCE(SUM(groq_completion_tokens), 0) AS groq_completion
|
|
297
|
+
FROM events
|
|
298
|
+
WHERE {where}
|
|
299
|
+
""")
|