feat(agent-office): 텔레그램 자연어 대화 + 프롬프트 캐싱 + 평가 지표

- 슬래시 명령이 아닌 메시지를 Claude Haiku 4.5로 응답
- system 프롬프트 + 히스토리 끝 블록에 cache_control:ephemeral 적용
- conversation_messages 테이블에 토큰·캐시·latency 기록
- chat_id 화이트리스트 + 분당 rate limit
- GET /api/agent-office/conversation/stats 로 캐시 히트율·토큰 확인
This commit is contained in:
2026-04-15 03:10:19 +09:00
parent c15ea96e2f
commit c019ab1681
6 changed files with 310 additions and 0 deletions

View File

@@ -67,6 +67,25 @@ def init_db() -> None:
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS conversation_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
model TEXT,
tokens_input INTEGER DEFAULT 0,
tokens_output INTEGER DEFAULT 0,
cache_read INTEGER DEFAULT 0,
cache_write INTEGER DEFAULT 0,
latency_ms INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_conv_chat
ON conversation_messages(chat_id, created_at DESC)
""")
# Seed default agent configs
for agent_id, name in [
("stock", "주식 트레이더"),
@@ -319,6 +338,109 @@ def get_token_usage_stats(agent_id: str, days: int = 1) -> dict:
}
def save_conversation_message(
chat_id: str,
role: str,
content: str,
model: Optional[str] = None,
tokens_input: int = 0,
tokens_output: int = 0,
cache_read: int = 0,
cache_write: int = 0,
latency_ms: int = 0,
) -> None:
with _conn() as conn:
conn.execute(
"""
INSERT INTO conversation_messages
(chat_id, role, content, model, tokens_input, tokens_output,
cache_read, cache_write, latency_ms)
VALUES (?,?,?,?,?,?,?,?,?)
""",
(str(chat_id), role, content, model, tokens_input, tokens_output,
cache_read, cache_write, latency_ms),
)
def get_conversation_history(chat_id: str, limit: int = 20) -> List[Dict[str, Any]]:
"""최근 N개를 시간순(오래된 → 최신)으로 반환."""
with _conn() as conn:
rows = conn.execute(
"""
SELECT role, content FROM conversation_messages
WHERE chat_id=? ORDER BY id DESC LIMIT ?
""",
(str(chat_id), limit),
).fetchall()
return [{"role": r["role"], "content": r["content"]} for r in reversed(rows)]
def count_recent_user_messages(chat_id: str, seconds: int = 60) -> int:
with _conn() as conn:
r = conn.execute(
"""
SELECT COUNT(*) AS c FROM conversation_messages
WHERE chat_id=? AND role='user'
AND created_at >= strftime('%Y-%m-%dT%H:%M:%fZ','now', ?)
""",
(str(chat_id), f"-{int(seconds)} seconds"),
).fetchone()
return r["c"] if r else 0
def get_conversation_stats(days: int = 7) -> Dict[str, Any]:
with _conn() as conn:
rows = conn.execute(
"""
SELECT chat_id,
COUNT(*) AS msg_count,
SUM(tokens_input) AS in_tokens,
SUM(tokens_output) AS out_tokens,
SUM(cache_read) AS cache_read,
SUM(cache_write) AS cache_write,
AVG(latency_ms) AS avg_latency
FROM conversation_messages
WHERE role='assistant'
AND created_at >= strftime('%Y-%m-%dT%H:%M:%fZ','now', ?)
GROUP BY chat_id
""",
(f"-{int(days)} days",),
).fetchall()
by_chat = []
tot_in = tot_out = tot_r = tot_w = tot_msgs = 0
for r in rows:
ci = int(r["in_tokens"] or 0)
co = int(r["out_tokens"] or 0)
cr = int(r["cache_read"] or 0)
cw = int(r["cache_write"] or 0)
mc = int(r["msg_count"] or 0)
hit_rate = (cr / (cr + cw)) if (cr + cw) > 0 else 0.0
by_chat.append({
"chat_id": r["chat_id"],
"message_count": mc,
"tokens_input": ci,
"tokens_output": co,
"cache_read": cr,
"cache_write": cw,
"cache_hit_rate": round(hit_rate, 3),
"avg_latency_ms": round(float(r["avg_latency"] or 0), 1),
})
tot_in += ci; tot_out += co; tot_r += cr; tot_w += cw; tot_msgs += mc
overall_hit = (tot_r / (tot_r + tot_w)) if (tot_r + tot_w) > 0 else 0.0
return {
"days": days,
"total_messages": tot_msgs,
"tokens_input": tot_in,
"tokens_output": tot_out,
"cache_read": tot_r,
"cache_write": tot_w,
"cache_hit_rate": round(overall_hit, 3),
"by_chat": by_chat,
}
def get_activity_feed(limit: int = 50, offset: int = 0) -> dict:
with _conn() as conn:
total_row = conn.execute("""