73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""Antigravity CLI headless backend. `agy -p` subprocess + 드라이버 스키마 검증·재시도."""
|
|
import asyncio
|
|
import json
|
|
import re
|
|
import tempfile
|
|
from pydantic import BaseModel, ValidationError
|
|
from deep_research.backends.base import AgentBackend
|
|
|
|
_FENCE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL)
|
|
|
|
|
|
def extract_json_block(text: str) -> dict | None:
|
|
"""출력 텍스트에서 JSON 객체 추출: 펜스 우선, 없으면 최외곽 중괄호."""
|
|
m = _FENCE.search(text)
|
|
candidates = [m.group(1)] if m else []
|
|
if not candidates:
|
|
start, depth = -1, 0
|
|
for i, ch in enumerate(text):
|
|
if ch == "{":
|
|
if depth == 0:
|
|
start = i
|
|
depth += 1
|
|
elif ch == "}" and depth > 0:
|
|
depth -= 1
|
|
if depth == 0 and start >= 0:
|
|
candidates.append(text[start:i + 1])
|
|
break
|
|
for c in candidates:
|
|
try:
|
|
return json.loads(c)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
return None
|
|
|
|
|
|
class AntigravityBackend(AgentBackend):
|
|
def __init__(self, *, retries: int = 2, neutral_cwd: str | None = None, model: str | None = None):
|
|
self.retries = retries
|
|
self.neutral_cwd = neutral_cwd or tempfile.gettempdir()
|
|
self.model = model
|
|
|
|
async def _invoke(self, prompt: str) -> str | None:
|
|
# 실 CLI 스모크 (2026-06-10): agy 에 `--cd`/`-m` 플래그 없음 ("flags provided but
|
|
# not defined" 즉사) — cwd 는 subprocess 인자로, 모델은 `--model`. stdin 차단(비-tty 안전).
|
|
cmd = ["agy", "-p", prompt]
|
|
if self.model:
|
|
cmd += ["--model", self.model]
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*cmd, cwd=self.neutral_cwd, stdin=asyncio.subprocess.DEVNULL,
|
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
out, _ = await proc.communicate()
|
|
if proc.returncode != 0:
|
|
return None
|
|
return out.decode("utf-8", "replace")
|
|
|
|
async def run_agent(self, prompt: str, schema: type[BaseModel], *, label: str):
|
|
schema_json = json.dumps(schema.model_json_schema())
|
|
full = (prompt + "\n\n## Output format\nReturn ONLY a JSON object conforming to this "
|
|
"JSON Schema (no prose, no markdown fences):\n" + schema_json)
|
|
for attempt in range(self.retries + 1):
|
|
text = await self._invoke(full if attempt == 0 else
|
|
full + "\n\nYour previous output was invalid. Return ONLY the JSON object.")
|
|
if text is None:
|
|
continue
|
|
obj = extract_json_block(text)
|
|
if obj is None:
|
|
continue
|
|
try:
|
|
return schema.model_validate(obj)
|
|
except ValidationError:
|
|
continue
|
|
return None
|