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}