254 lines
7.0 KiB
TypeScript
254 lines
7.0 KiB
TypeScript
/**
|
|
* Level 4: Hermes Connection
|
|
*
|
|
* Integration with Hermes gateway (Telegram)
|
|
*
|
|
* Flow:
|
|
* Telegram → Hermes → This Server → Queue → Worker → Response → Hermes → Telegram
|
|
*
|
|
* This creates a simple HTTP server that Hermes can call via webhook or tool.
|
|
*/
|
|
|
|
import { Agent, type AgentTool, type AgentMessage, type AgentEvent } from "@mariozechner/pi-agent-core";
|
|
import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
|
|
import type { Model } from "@mariozechner/pi-ai";
|
|
import { exec } from "child_process";
|
|
import http from "http";
|
|
|
|
// ============== CONFIG ==============
|
|
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || "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";
|
|
process.env.OPENROUTER_API_KEY = OPENROUTER_API_KEY;
|
|
|
|
const model: Model<"openai-responses"> = {
|
|
id: "stepfun/step-3.5-flash:free",
|
|
name: "Step-3.5 Flash (Free)",
|
|
api: "openai-responses",
|
|
provider: "openrouter",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
output: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 128000,
|
|
maxTokens: 8192,
|
|
};
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// ============== TASK QUEUE (Simplified) ==============
|
|
interface Task {
|
|
id: string;
|
|
message: string;
|
|
chatId: string;
|
|
status: "pending" | "running" | "completed";
|
|
response?: string;
|
|
}
|
|
|
|
class SimpleQueue {
|
|
private tasks: Task[] = [];
|
|
private processing = false;
|
|
|
|
add(message: string, chatId: string): string {
|
|
const id = `task-${Date.now()}`;
|
|
this.tasks.push({ id, message, chatId, status: "pending" });
|
|
return id;
|
|
}
|
|
|
|
getNext(): Task | undefined {
|
|
const task = this.tasks.find(t => t.status === "pending");
|
|
if (task) {
|
|
task.status = "running";
|
|
}
|
|
return task;
|
|
}
|
|
|
|
complete(id: string, response: string) {
|
|
const task = this.tasks.find(t => t.id === id);
|
|
if (task) {
|
|
task.status = "completed";
|
|
task.response = response;
|
|
}
|
|
}
|
|
|
|
getByChat(chatId: string): Task | undefined {
|
|
return this.tasks.find(t => t.chatId === chatId && t.status === "completed");
|
|
}
|
|
|
|
size(): number {
|
|
return this.tasks.length;
|
|
}
|
|
}
|
|
|
|
// ============== AGENT ==============
|
|
class HermesAgent {
|
|
private agent: Agent;
|
|
private chatId?: string;
|
|
|
|
constructor(chatId?: string) {
|
|
this.chatId = chatId;
|
|
this.agent = new Agent({
|
|
initialState: {
|
|
systemPrompt: "You are a helpful coding assistant. Be concise and helpful.",
|
|
model: model,
|
|
tools: [] as any,
|
|
messages: [],
|
|
},
|
|
convertToLlm: (messages: AgentMessage[]) => {
|
|
return messages
|
|
.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult")
|
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
},
|
|
});
|
|
}
|
|
|
|
async process(message: string): Promise<string> {
|
|
let response = "";
|
|
|
|
this.agent.subscribe((event) => {
|
|
if (event.type === "message_update") {
|
|
const ev = event as any;
|
|
if (ev.assistantMessageEvent?.type === "text_delta") {
|
|
response += ev.assistantMessageEvent.delta || "";
|
|
}
|
|
}
|
|
});
|
|
|
|
await this.agent.prompt(message);
|
|
return response || "No response";
|
|
}
|
|
}
|
|
|
|
// ============== HTTP SERVER ==============
|
|
const queue = new SimpleQueue();
|
|
const agent = new HermesAgent();
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
// CORS headers
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
|
|
if (req.method === "OPTIONS") {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Parse URL
|
|
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
|
|
|
|
// Routes
|
|
if (url.pathname === "/health") {
|
|
// Health check
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ status: "ok", queueSize: queue.size() }));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/webhook" && req.method === "POST") {
|
|
// Receive message from Hermes (Telegram)
|
|
let body = "";
|
|
req.on("data", chunk => body += chunk);
|
|
req.on("end", async () => {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
const message = data.message || data.text || data.content;
|
|
const chatId = data.chat_id || data.chatId || data.from?.id || "unknown";
|
|
|
|
console.log(`📥 Received from chat ${chatId}: ${message.substring(0, 50)}...`);
|
|
|
|
// Process with agent
|
|
const response = await agent.process(message);
|
|
|
|
console.log(`📤 Sending response: ${response.substring(0, 50)}...`);
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
response,
|
|
chatId,
|
|
}));
|
|
} catch (error: any) {
|
|
console.error("❌ Error:", error.message);
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/message" && req.method === "POST") {
|
|
// Alternative endpoint: send message directly
|
|
let body = "";
|
|
req.on("data", chunk => body += chunk);
|
|
req.on("end", async () => {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
const message = data.message;
|
|
const chatId = data.chatId || "default";
|
|
|
|
console.log(`📥 Message from ${chatId}: ${message}`);
|
|
|
|
const response = await agent.process(message);
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ response }));
|
|
} catch (error: any) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/status" && req.method === "GET") {
|
|
// Get status
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({
|
|
status: "running",
|
|
queueSize: queue.size(),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 404
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
});
|
|
|
|
// ============== MAIN ==============
|
|
async function main() {
|
|
console.log("🧪 Level 4: Hermes Connection\n");
|
|
|
|
registerBuiltInApiProviders();
|
|
|
|
// Start server
|
|
server.listen(PORT, () => {
|
|
console.log(`
|
|
🚀 Server running on http://localhost:${PORT}
|
|
|
|
📡 Endpoints:
|
|
GET /health - Health check
|
|
GET /status - Server status
|
|
POST /webhook - Receive from Hermes (Telegram)
|
|
POST /message - Send message directly
|
|
|
|
📝 Example curl:
|
|
curl -X POST http://localhost:${PORT}/message \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{"message": "Hello!", "chatId": "123"}'
|
|
`);
|
|
});
|
|
|
|
// Handle shutdown
|
|
process.on("SIGINT", () => {
|
|
console.log("\n🛑 Shutting down...");
|
|
server.close(() => {
|
|
console.log("✅ Server stopped");
|
|
process.exit(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
main().catch(console.error);
|