feature/pi-refactor
1/**
2 * XMPP Agent Wrapper
3 * Wraps Pi's Agent class with XMPP-specific concerns
4 */
5
6import { Agent, type AgentMessage, type AgentTool } from "@mariozechner/pi-agent-core";
7import type { Model } from "@mariozechner/pi-ai";
8import { getModelByPrefix, parseModelPrefix, getAllModelsFormatted } from "./config.js";
9
10const HELP_TEXT = `Available commands:
11/help - Show this help message
12/ping - Check if bot is alive
13/clear - Clear conversation history
14/models - List available models
15/stats - Show session statistics
16
17Model prefixes:
18opus:, o: - Claude Opus 4
19sonnet:, s: - Claude Sonnet 4
20haiku:, h: - Claude Haiku
21g:, gemini: - Gemini 2.0 Flash
22gp: - Gemini 2.5 Pro
23gpt:, gpt4: - GPT-4o
24o1: - OpenAI o1
25o3: - OpenAI o3-mini
26
27Example: "opus: Explain quantum computing"
28`;
29
30export class XmppAgent {
31 public readonly jid: string;
32 private agent: Agent;
33 private currentModel: Model<any>;
34 private tools: AgentTool[];
35 private debug: boolean;
36
37 constructor(jid: string, defaultModel: Model<any>, tools: AgentTool[] = [], debug: boolean = false) {
38 this.jid = jid;
39 this.currentModel = defaultModel;
40 this.tools = tools;
41 this.debug = debug;
42
43 if (debug) {
44 console.log(`[Agent:${jid}] Created with model: ${defaultModel.provider}/${defaultModel.id}`);
45 console.log(`[Agent:${jid}] Tools available: ${tools.map(t => t.name).join(", ")}`);
46 }
47
48 // Initialize Pi Agent
49 this.agent = new Agent({
50 initialState: {
51 model: defaultModel,
52 tools: tools,
53 systemPrompt: "You are a helpful research assistant accessible via XMPP. Provide clear, concise, and accurate responses. Use available tools when appropriate to provide accurate information.",
54 messages: [],
55 },
56 });
57
58 // Subscribe to agent events for logging
59 if (debug) {
60 this.agent.subscribe((event) => {
61 if (event.type === "turn_start") {
62 console.log(`[Agent:${jid}] Turn started`);
63 } else if (event.type === "turn_end") {
64 const toolCalls = event.toolResults?.length || 0;
65 console.log(`[Agent:${jid}] Turn ended (${toolCalls} tool calls)`);
66 }
67 });
68 }
69 }
70
71 /**
72 * Process a message from XMPP
73 * Handles slash commands and model prefixes
74 */
75 async processMessage(body: string): Promise<string> {
76 const startTime = Date.now();
77
78 // Handle slash commands
79 if (body.startsWith("/")) {
80 if (this.debug) {
81 console.log(`[Agent:${this.jid}] Processing slash command: ${body}`);
82 }
83 return this.handleCommand(body);
84 }
85
86 // Parse model prefix
87 const { prefix, content } = parseModelPrefix(body);
88
89 // Switch model if prefix found
90 if (prefix) {
91 const newModel = getModelByPrefix(prefix);
92 if (newModel) {
93 if (this.debug) {
94 console.log(`[Agent:${this.jid}] Switching model: ${this.currentModel.provider}/${this.currentModel.id} → ${newModel.provider}/${newModel.id}`);
95 }
96 this.currentModel = newModel;
97 this.agent.setModel(newModel);
98 }
99 }
100
101 if (this.debug) {
102 console.log(`[Agent:${this.jid}] Processing message with model: ${this.currentModel.provider}/${this.currentModel.id}`);
103 }
104
105 // Send message to agent
106 await this.agent.prompt(content);
107
108 // Get last assistant message
109 const response = this.getLastAssistantMessage();
110
111 const elapsed = Date.now() - startTime;
112 if (this.debug) {
113 console.log(`[Agent:${this.jid}] Response generated in ${elapsed}ms (${response.length} chars)`);
114 }
115
116 return response;
117 }
118
119 /**
120 * Handle slash commands
121 */
122 private async handleCommand(command: string): Promise<string> {
123 const cmd = command.toLowerCase().trim();
124
125 if (cmd === "/ping") {
126 return "pong!";
127 }
128
129 if (cmd === "/help") {
130 return HELP_TEXT;
131 }
132
133 if (cmd === "/clear") {
134 this.agent.clearMessages();
135 return "Conversation cleared.";
136 }
137
138 if (cmd === "/models") {
139 return getAllModelsFormatted();
140 }
141
142 if (cmd === "/stats") {
143 const messages = this.agent.state.messages;
144 const userMessages = messages.filter((m) => m.role === "user").length;
145 const assistantMessages = messages.filter((m) => m.role === "assistant").length;
146
147 return `Session statistics:
148Total messages: ${messages.length}
149User messages: ${userMessages}
150Assistant messages: ${assistantMessages}
151Current model: ${this.currentModel.provider}/${this.currentModel.id}`;
152 }
153
154 return `Unknown command: ${command}. Type /help for available commands.`;
155 }
156
157 /**
158 * Get last assistant message from conversation
159 */
160 private getLastAssistantMessage(): string {
161 const messages = this.agent.state.messages;
162
163 // Find last assistant message
164 for (let i = messages.length - 1; i >= 0; i--) {
165 const msg = messages[i];
166 if (msg.role === "assistant") {
167 return this.extractTextContent(msg);
168 }
169 }
170
171 return "No response generated.";
172 }
173
174 /**
175 * Extract text content from AgentMessage
176 */
177 private extractTextContent(message: AgentMessage): string {
178 if (typeof message.content === "string") {
179 return message.content;
180 }
181
182 if (Array.isArray(message.content)) {
183 const textParts = message.content
184 .filter((part) => part.type === "text")
185 .map((part) => (part as any).text)
186 .join("\n");
187 return textParts || "No text content.";
188 }
189
190 return "Unable to extract message content.";
191 }
192
193 /**
194 * Get all messages in conversation
195 */
196 getMessages(): AgentMessage[] {
197 return this.agent.state.messages;
198 }
199
200 /**
201 * Get total message count
202 */
203 getMessageCount(): number {
204 return this.agent.state.messages.length;
205 }
206
207 /**
208 * Get current model
209 */
210 getCurrentModel(): Model<any> {
211 return this.currentModel;
212 }
213
214 /**
215 * Get tools available to this agent
216 */
217 getTools(): AgentTool[] {
218 return this.tools;
219 }
220}