agentops/examples/xpander/coding_agent.py

179 lines
7.0 KiB
Python

"""
Copyright (c) 2025 Xpander, Inc. All rights reserved.
Modified to use AgentOps callback handlers for tool instrumentation.
Single-file implementation combining MyAgent and XpanderEventListener.
"""
# ruff: noqa: E402
import asyncio
import json
import os
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
from loguru import logger
load_dotenv()
import agentops
print("🔧 Initializing AgentOps...")
agentops.init(
api_key=os.getenv("AGENTOPS_API_KEY"),
trace_name="my-xpander-coding-agent-callbacks",
default_tags=["xpander", "coding-agent", "callbacks"],
)
print("✅ AgentOps initialized")
print("📦 Importing xpander_sdk...")
from xpander_sdk import XpanderClient, LLMProvider, LLMTokens, Tokens, Agent
from xpander_utils.events import XpanderEventListener, AgentExecutionResult, AgentExecution, ExecutionStatus
from openai import AsyncOpenAI
# Simple logger setup
logger.remove()
logger.add(sys.stderr, format="{time:HH:mm:ss} | {message}", level="INFO")
class MyAgent:
def __init__(self):
logger.info("🚀 Initializing MyAgent...")
# Load config
config_path = Path(__file__).parent / "xpander_config.json"
config = json.loads(config_path.read_text())
# Get API keys
xpander_key = config.get("api_key") or os.getenv("XPANDER_API_KEY")
agent_id = config.get("agent_id") or os.getenv("XPANDER_AGENT_ID")
openai_key = os.getenv("OPENAI_API_KEY")
if not all([xpander_key, agent_id, openai_key]):
raise ValueError("Missing required API keys")
# Initialize
self.openai = AsyncOpenAI(api_key=openai_key)
xpander_client = XpanderClient(api_key=xpander_key)
self.agent_backend: Agent = xpander_client.agents.get(agent_id=agent_id)
self.agent_backend.select_llm_provider(LLMProvider.OPEN_AI)
logger.info(f"Agent: {self.agent_backend.name}")
logger.info(f"Tools: {len(self.agent_backend.tools)} available")
logger.info("✅ Ready!")
async def run(self, user_txt_input: str) -> dict:
step = 0
start_time = time.perf_counter()
tokens = Tokens(worker=LLMTokens(0, 0, 0))
try:
while not self.agent_backend.is_finished():
step += 1
logger.info(f"Step {step} - Calling LLM...")
response = await self.openai.chat.completions.create(
model="gpt-4.1",
messages=self.agent_backend.messages,
tools=self.agent_backend.get_tools(),
tool_choice=self.agent_backend.tool_choice,
temperature=0,
)
if hasattr(response, "usage"):
tokens.worker.prompt_tokens += response.usage.prompt_tokens
tokens.worker.completion_tokens += response.usage.completion_tokens
tokens.worker.total_tokens += response.usage.total_tokens
self.agent_backend.add_messages(response.model_dump())
self.agent_backend.report_execution_metrics(llm_tokens=tokens, ai_model="gpt-4.1")
tool_calls = self.agent_backend.extract_tool_calls(response.model_dump())
if tool_calls:
logger.info(f"Executing {len(tool_calls)} tools...")
tool_results = await asyncio.to_thread(self.agent_backend.run_tools, tool_calls)
for res in tool_results:
emoji = "" if res.is_success else ""
logger.info(f"Tool result: {emoji} {res.function_name}")
duration = time.perf_counter() - start_time
logger.info(f"Done! Duration: {duration:.1f}s | Total tokens: {tokens.worker.total_tokens}")
result = self.agent_backend.retrieve_execution_result()
return {"result": result.result, "thread_id": result.memory_thread_id}
except Exception as e:
logger.error(f"Exception: {e}")
raise
# === Load Configuration ===
logger.info("[xpander_handler] Loading xpander_config.json")
config_path = Path(__file__).parent / "xpander_config.json"
with open(config_path, "r") as config_file:
xpander_config: dict = json.load(config_file)
logger.info(f"[xpander_handler] Loaded config: {xpander_config}")
# === Initialize Event Listener ===
logger.info(f"[xpander_handler] Initializing XpanderEventListener with config: {xpander_config}")
listener = XpanderEventListener(**xpander_config)
logger.info(f"[xpander_handler] Listener initialized: {listener}")
# === Define Execution Handler ===
async def on_execution_request(execution_task: AgentExecution) -> AgentExecutionResult:
logger.info(f"[on_execution_request] Called with execution_task: {execution_task}")
my_agent = MyAgent()
logger.info(f"[on_execution_request] Instantiated MyAgent: {my_agent}")
user_info = ""
user = getattr(execution_task.input, "user", None)
if user:
name = f"{user.first_name} {user.last_name}".strip()
email = getattr(user, "email", "")
user_info = f"👤 From user: {name}\n📧 Email: {email}"
IncomingEvent = f"\n📨 Incoming message: {execution_task.input.text}\n{user_info}"
logger.info(f"[on_execution_request] IncomingEvent: {IncomingEvent}")
logger.info(f"[on_execution_request] Calling agent_backend.init_task with execution={execution_task.model_dump()}")
my_agent.agent_backend.init_task(execution=execution_task.model_dump())
# extract just the text input for quick start purpose. for more robust use the object
user_txt_input = execution_task.input.text
logger.info(f"[on_execution_request] Running agent with user_txt_input: {user_txt_input}")
try:
await my_agent.run(user_txt_input)
logger.info("[on_execution_request] Agent run completed")
execution_result = my_agent.agent_backend.retrieve_execution_result()
logger.info(f"[on_execution_request] Execution result: {execution_result}")
result_obj = AgentExecutionResult(
result=execution_result.result,
is_success=execution_result.status == ExecutionStatus.COMPLETED,
)
logger.info(f"[on_execution_request] Returning AgentExecutionResult: {result_obj}")
return result_obj
except Exception as e:
logger.error(f"[on_execution_request] Exception: {e}")
raise
finally:
logger.info("[on_execution_request] Exiting handler")
# === Register Callback ===
logger.info("[xpander_handler] Registering on_execution_request callback")
listener.register(on_execution_request=on_execution_request)
logger.info("[xpander_handler] Callback registered")
# Example usage for direct interaction
if __name__ == "__main__":
async def main():
agent = MyAgent()
while True:
task = input("\nAsk Anything (Type exit to end) \nInput: ")
if task.lower() == "exit":
break
agent.agent_backend.add_task(input=task)
result = await agent.run(task)
print(f"\nResult: {result['result']}")
asyncio.run(main())