CoolFace
Apppublic

parthpetkar/metahackathon

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
api_client.py187 linesDownload Raw Back to agent
1"""CI/CD API tool dispatcher — bridges agent tool calls to the WebSocket API.
2
3The agent's tool loop calls ``execute_tool(tool_name, arguments, client)``
4where ``client`` is a ``SyncCICDWebSocketClient`` held open for the episode.
5
6Supported tools (matching API_TOOL_SCHEMAS in api_tool_schemas.py):
7  read_file          → client.read_file(path)
8  write_file         → client.write_file(path, content)
9  list_files         → client.list_files(directory)
10  trigger_pipeline   → client.trigger_pipeline()  [blocks until pipeline_done]
11  set_hypothesis     → local — returns {"success": True, "acknowledged": True}
12  finalize           → local — returns {"success": True, "finalized": True}
13
14Errors are caught and returned as {"success": False, "error": "..."} so the
15agent's loop can surface them as tool results without crashing.
16"""
17
18from __future__ import annotations
19
20import logging
21import os
22from typing import Any, Dict, Optional, Tuple
23
24logger = logging.getLogger(__name__)
25
26# Lazy import — ws_client requires websockets which may not be installed
27_ws_client_module = None
28
29
30def _get_ws_client_module():
31    global _ws_client_module
32    if _ws_client_module is None:
33        from agent import ws_client as m
34        _ws_client_module = m
35    return _ws_client_module
36
37
38def create_ws_client(
39    workspace_id: str,
40    base_url: Optional[str] = None,
41    timeout: float = 30.0,
42    pipeline_timeout: float = 300.0,
43):
44    """Create and return a SyncCICDWebSocketClient (not yet connected).
45
46    Caller must use it as a context manager or call connect()/close() manually.
47    """
48    url = base_url or os.getenv("CICD_API_WS_URL", "ws://localhost:8001")
49    mod = _get_ws_client_module()
50    return mod.SyncCICDWebSocketClient(
51        workspace_id=workspace_id,
52        base_url=url,
53        timeout=timeout,
54        pipeline_timeout=pipeline_timeout,
55    )
56
57
58def execute_tool(
59    tool_name: str,
60    arguments: Dict[str, Any],
61    client,  # SyncCICDWebSocketClient
62) -> Dict[str, Any]:
63    """Dispatch a tool call to the CI/CD WebSocket API.
64
65    Args:
66        tool_name:  Name of the tool (must match API_TOOL_SCHEMAS).
67        arguments:  Parsed tool arguments dict from the LLM response.
68        client:     An open SyncCICDWebSocketClient instance.
69
70    Returns:
71        A dict with at least {"success": bool} and tool-specific fields.
72    """
73    try:
74        if tool_name == "read_file":
75            path = arguments.get("path", "")
76            if not path:
77                return {"success": False, "error": "read_file requires 'path'"}
78            exists, content = client.read_file(path)
79            return {"success": exists, "path": path, "content": content, "exists": exists}
80
81        elif tool_name == "write_file":
82            path = arguments.get("path", "")
83            content = arguments.get("content", "")
84            if not path:
85                return {"success": False, "error": "write_file requires 'path'"}
86            ok = client.write_file(path, content)
87            return {
88                "success": ok,
89                "path": path,
90                "message": f"File {path} {'updated' if ok else 'update failed'}",
91            }
92
93        elif tool_name == "list_files":
94            directory = arguments.get("directory", "")
95            files, directories = client.list_files(directory)
96            return {"success": True, "files": files, "directories": directories}
97
98        elif tool_name == "trigger_pipeline":
99            result = client.trigger_pipeline()
100            stage_summaries = {}
101            for stage, detail in result.stage_details.items():
102                stage_summaries[stage] = {
103                    "status": detail.get("status"),
104                    "duration": detail.get("duration"),
105                    "logs": (detail.get("logs") or "")[:2000],  # cap log size per stage
106                }
107            return {
108                "success": True,
109                "job_id": result.job_id,
110                "status": result.status,
111                "passed": result.passed,
112                "failed_stage": result.failed_stage,
113                "duration": result.duration,
114                "stages": stage_summaries,
115            }
116
117        elif tool_name == "set_hypothesis":
118            hypothesis = arguments.get("hypothesis", "")
119            if not hypothesis:
120                return {"success": False, "error": "set_hypothesis requires 'hypothesis'"}
121            return {"success": True, "acknowledged": True, "hypothesis": hypothesis}
122
123        elif tool_name == "finalize":
124            return {"success": True, "finalized": True}
125
126        else:
127            return {"success": False, "error": f"Unknown tool: {tool_name!r}"}
128
129    except Exception as exc:
130        logger.error("Tool %s failed: %s", tool_name, exc, exc_info=True)
131        return {"success": False, "error": str(exc)}
132
133
134# ── Convenience: format tool result as a human-readable string ──────────────
135
136def format_tool_result(tool_name: str, result: Dict[str, Any]) -> str:
137    """Convert a tool result dict into a compact string for the LLM context."""
138    if not result.get("success"):
139        return f"[{tool_name}] ERROR: {result.get('error', 'unknown error')}"
140
141    if tool_name == "read_file":
142        if not result.get("exists"):
143            return f"[read_file] File not found: {result.get('path')}"
144        content = result.get("content", "")
145        lines = content.splitlines()
146        preview = "\n".join(lines[:200])
147        suffix = f"\n... ({len(lines) - 200} more lines)" if len(lines) > 200 else ""
148        return f"[read_file] {result['path']} ({len(lines)} lines):\n{preview}{suffix}"
149
150    if tool_name == "write_file":
151        return f"[write_file] {result.get('message', 'done')}"
152
153    if tool_name == "list_files":
154        files = result.get("files", [])
155        dirs = result.get("directories", [])
156        parts = []
157        if dirs:
158            parts.append("Directories:\n  " + "\n  ".join(sorted(dirs)))
159        if files:
160            parts.append("Files:\n  " + "\n  ".join(sorted(files)))
161        return "[list_files]\n" + "\n".join(parts) if parts else "[list_files] (empty directory)"
162
163    if tool_name == "trigger_pipeline":
164        status = result.get("status", "unknown")
165        failed = result.get("failed_stage")
166        dur = result.get("duration")
167        header = f"[trigger_pipeline] status={status}"
168        if failed:
169            header += f"  failed_stage={failed}"
170        if dur:
171            header += f"  duration={dur:.1f}s"
172        stages = result.get("stages", {})
173        stage_lines = []
174        for stage, detail in stages.items():
175            st = detail.get("status", "?")
176            logs = (detail.get("logs") or "").strip()
177            stage_lines.append(f"\n--- {stage}: {st} ---\n{logs}" if logs else f"\n--- {stage}: {st} ---")
178        return header + "".join(stage_lines)
179
180    if tool_name == "set_hypothesis":
181        return f"[set_hypothesis] Hypothesis recorded: {result.get('hypothesis', '')}"
182
183    if tool_name == "finalize":
184        return "[finalize] Episode finalised — awaiting score."
185
186    return f"[{tool_name}] {result}"
187 
parthpetkar/metahackathon · CoolFace