-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent.ts
More file actions
175 lines (143 loc) · 5.94 KB
/
agent.ts
File metadata and controls
175 lines (143 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import OpenAI from "openai";
import { loadConfig } from "./config";
import { getDb, marks as marksTable } from "./db";
import { readMemory, writeMemory, getRecentDayLogs, appendDayLog } from "./memory";
import { desc, gte } from "drizzle-orm";
const SYSTEM_PROMPT = `Ты — AI-агент для мониторинга успеваемости школьника.
Твоя задача — анализировать оценки, находить паттерны, предупреждать о проблемах.
У тебя есть доступ к:
1. MEMORY.md — твоя долгосрочная память, ты сам её обновляешь
2. Дневные логи — append-only записи о новых оценках
3. База оценок — все оценки в SQLite
Формат MEMORY.md который ты должен поддерживать:
\`\`\`
# Сводка по ученику
Обновлено: YYYY-MM-DD
## Слабые места
- Предмет: описание проблемы
## Паттерны
- Наблюдение
## Сильные предметы
- Предмет: средняя, тренд
\`\`\`
Когда тебя вызывают после синхронизации:
- Проанализируй новые оценки
- Сравни с историей
- Если есть тревожные сигналы — верни ALERT
- Обнови MEMORY.md
Когда пользователь спрашивает в чате — отвечай по-русски, конкретно и полезно.
Используй данные из памяти и базы.`;
function getLlmClient() {
const config = loadConfig();
return new OpenAI({
baseURL: config.llm.base_url,
apiKey: config.llm.api_key,
});
}
function getRecentMarksContext(days: number): string {
const db = getDb();
const since = new Date();
since.setDate(since.getDate() - days);
const dateStr = since.toISOString().slice(0, 10);
const rows = db
.select()
.from(marksTable)
.where(gte(marksTable.date, dateStr))
.orderBy(desc(marksTable.date))
.all();
if (rows.length === 0) return "Нет оценок за последние " + days + " дней.";
const bySubject: Record<string, { values: number[]; weights: number[] }> = {};
for (const r of rows) {
if (!bySubject[r.subject]) {
bySubject[r.subject] = { values: [], weights: [] };
}
bySubject[r.subject].values.push(r.value);
bySubject[r.subject].weights.push(r.weight);
}
const lines: string[] = ["## Оценки за последние " + days + " дней\n"];
for (const [subj, data] of Object.entries(bySubject)) {
const weighted =
data.values.reduce((sum, v, i) => sum + v * data.weights[i], 0) /
data.weights.reduce((a, b) => a + b, 0);
lines.push(`**${subj}**: ${data.values.join(", ")} (средневзв. ${weighted.toFixed(1)})`);
}
return lines.join("\n");
}
export async function runAgent(
trigger: "sync" | "chat" | "weekly",
userMessage?: string
): Promise<string> {
const config = loadConfig();
const client = getLlmClient();
const memory = readMemory();
const recentLogs = getRecentDayLogs(7);
const marksContext = getRecentMarksContext(30);
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: SYSTEM_PROMPT },
{
role: "user",
content: `## Текущая память (MEMORY.md)
${memory || "(пусто — создай начальную сводку)"}
## Логи за последние 7 дней
${recentLogs || "(нет логов)"}
## Данные из базы
${marksContext}
---
Триггер: ${trigger}
${trigger === "chat" && userMessage ? `Вопрос пользователя: ${userMessage}` : "Проанализируй новые данные, обнови память, отправь алерт если нужно."}
Ответь в формате:
MEMORY_UPDATE:
<новое содержимое MEMORY.md если нужно обновить, или SKIP>
ALERT:
<текст алерта для Telegram если нужен, или NONE>
RESPONSE:
<ответ пользователю если это чат, или краткий лог>`,
},
];
const completion = await client.chat.completions.create({
model: config.llm.model,
messages,
temperature: 0.3,
max_tokens: 2000,
});
const reply = completion.choices[0]?.message?.content ?? "";
// Parse structured response
const memoryMatch = reply.match(/MEMORY_UPDATE:\s*\n([\s\S]*?)(?=\nALERT:)/);
const alertMatch = reply.match(/ALERT:\s*\n([\s\S]*?)(?=\nRESPONSE:)/);
const responseMatch = reply.match(/RESPONSE:\s*\n([\s\S]*?)$/);
// Update memory if needed
if (memoryMatch) {
const newMemory = memoryMatch[1].trim();
if (newMemory && newMemory !== "SKIP") {
writeMemory(newMemory);
console.log("[agent] Updated MEMORY.md");
}
}
// Send alert if needed
if (alertMatch) {
const alertText = alertMatch[1].trim();
if (alertText && alertText !== "NONE" && config.alerts) {
await sendTelegramAlert(config.alerts.telegram_token, config.alerts.chat_id, alertText);
console.log("[agent] Sent Telegram alert");
}
}
// Log agent run
const today = new Date().toISOString().slice(0, 10);
const responseText = responseMatch?.[1]?.trim() ?? reply;
appendDayLog(today, `### Agent (${trigger}) ${new Date().toISOString()}\n${responseText}`);
return responseText;
}
async function sendTelegramAlert(token: string, chatId: string, text: string) {
const url = `https://api.telegram.org/bot${token}/sendMessage`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" }),
});
}
// If run standalone (weekly cron)
if (process.argv[1]?.endsWith("agent.ts")) {
runAgent("weekly")
.then((r) => console.log("[agent] Done:", r))
.catch((e) => console.error("[agent] Error:", e));
}