spatial-atlas / src /agent.py
Arun0808's picture
Spatial Atlas v1.0: spatial-aware research agent for AgentBeats Challenge
77a8cec
Raw
History Blame Contribute Delete
6.31 kB
"""
Spatial Atlas — Core Orchestrator Agent
THE BRAIN: Receives A2A messages, classifies domain (FieldWorkArena vs MLE-Bench),
routes to the appropriate handler, and returns formatted artifacts.
"""
import base64
import logging
import traceback
from a2a.server.tasks import TaskUpdater
from a2a.types import (
DataPart,
FilePart,
FileWithBytes,
Message,
Part,
TaskState,
TextPart,
)
from a2a.utils import get_message_text, new_agent_text_message
from config import Config
from llm import LLMClient
from cost.tracker import CostTracker
from fieldwork.handler import FieldWorkHandler
from mlebench.handler import MLEBenchHandler
logger = logging.getLogger("spatial-atlas.agent")
class Agent:
def __init__(self):
self.config = Config()
self.llm = LLMClient(self.config)
self.cost_tracker = self.llm.cost_tracker
self.fieldwork = FieldWorkHandler(self.config, self.llm)
self.mlebench = MLEBenchHandler(self.config, self.llm)
self.messages: list[dict] = []
async def run(self, message: Message, updater: TaskUpdater) -> None:
"""Main entry point: classify domain and route to handler."""
await updater.update_status(
TaskState.working,
new_agent_text_message("Spatial Atlas processing..."),
)
try:
# Parse message into text and file parts
text_parts, file_parts = self._parse_message(message)
full_text = "\n".join(text_parts)
# Classify which benchmark is calling
domain = self._classify_domain(full_text, file_parts)
logger.info(f"Domain classified as: {domain}")
if domain == "mlebench":
await self._handle_mlebench(full_text, file_parts, updater)
else:
await self._handle_fieldwork(full_text, file_parts, updater)
logger.info(f"Task completed. {self.cost_tracker.summary()}")
except Exception as e:
logger.error(f"Agent error: {traceback.format_exc()}")
await updater.add_artifact(
parts=[Part(root=TextPart(text=f"Error: {e}"))],
name="Error",
)
async def _handle_fieldwork(
self,
text: str,
file_parts: list[tuple[str, str, str | bytes]],
updater: TaskUpdater,
) -> None:
"""Handle FieldWorkArena tasks."""
await updater.update_status(
TaskState.working,
new_agent_text_message("Analyzing field work task with spatial reasoning..."),
)
result = await self.fieldwork.handle(text, file_parts, updater)
await updater.add_artifact(
parts=[Part(root=TextPart(text=result))],
name="Analysis",
)
async def _handle_mlebench(
self,
text: str,
file_parts: list[tuple[str, str, str | bytes]],
updater: TaskUpdater,
) -> None:
"""Handle MLE-Bench tasks."""
await updater.update_status(
TaskState.working,
new_agent_text_message("Starting ML engineering pipeline..."),
)
csv_bytes, summary = await self.mlebench.handle(text, file_parts, updater)
parts = [Part(root=TextPart(text=summary))]
if csv_bytes:
b64_csv = base64.b64encode(csv_bytes).decode("ascii")
parts.append(
Part(
root=FilePart(
file=FileWithBytes(
bytes=b64_csv,
name="submission.csv",
mime_type="text/csv",
)
)
)
)
await updater.add_artifact(parts=parts, name="Submission")
def _parse_message(
self, message: Message
) -> tuple[list[str], list[tuple[str, str, str | bytes]]]:
"""
Parse A2A message into text content and file attachments.
Returns:
text_parts: List of text strings from the message
file_parts: List of (name, mime_type, data) tuples for file attachments
"""
text_parts: list[str] = []
file_parts: list[tuple[str, str, str | bytes]] = []
for part in message.parts:
root = part.root
if isinstance(root, TextPart):
text_parts.append(root.text)
elif isinstance(root, DataPart):
# Inline JSON data
import json
text_parts.append(json.dumps(root.data, indent=2))
elif isinstance(root, FilePart):
file_data = root.file
if isinstance(file_data, FileWithBytes):
name = file_data.name or "unknown"
mime = file_data.mime_type or "application/octet-stream"
data = file_data.bytes # base64-encoded string or bytes
file_parts.append((name, mime, data))
return text_parts, file_parts
def _classify_domain(
self,
text: str,
file_parts: list[tuple[str, str, str | bytes]],
) -> str:
"""
Classify which benchmark is calling based on message content.
Detection strategy:
1. MLE-Bench: has competition.tar.gz attachment
2. FieldWorkArena: has '# Question' + '# Output Format' in text
3. MLE-Bench: text mentions Kaggle/MLE-bench/competition
4. Default: FieldWorkArena
"""
# Check for MLE-Bench tar file
for name, mime, _data in file_parts:
if name and ("competition" in name.lower() or name.endswith(".tar.gz")):
return "mlebench"
if mime and "gzip" in mime:
return "mlebench"
# Check for FieldWorkArena goal format
text_lower = text.lower()
if "# question" in text_lower and "# output format" in text_lower:
return "fieldwork"
# Check for MLE-Bench keywords
mle_keywords = ["kaggle", "mle-bench", "competition", "submission.csv", "train a model"]
if any(kw in text_lower for kw in mle_keywords):
return "mlebench"
# Default to fieldwork (more common in research agent track)
return "fieldwork"