feat: select workspace by name via /workspaces/lookup
Add workspace_name input; the runner resolves it to a workspace_id through
GET {api_base}/workspaces/lookup?user_id=... before uploading the agent.
workspace_id remains as an explicit override. Name matching is NFC-normalized
(handles Korean NFD/NFC) with a case-insensitive fallback, and a no-match
error lists the available workspace names. Requires one of name/id.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,11 @@
|
||||
# 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000).
|
||||
#
|
||||
# 실행 흐름 (SKILL.md §0.6 경로 A):
|
||||
# upload-agent 등록 → SSE start → stage_complete 자동 confirm → execution_complete
|
||||
# workspaces/lookup(이름→UUID) → upload-agent 등록 → SSE start →
|
||||
# stage_complete 자동 confirm → execution_complete
|
||||
# stage_error 발생 시 세션을 cancel 하고 실패 처리한다.
|
||||
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
|
||||
# ※ workspace_name(작업실 이름) 또는 workspace_id(UUID) 중 하나는 반드시 지정.
|
||||
#
|
||||
# 실행 시간 상한 (주의):
|
||||
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
|
||||
@@ -48,10 +50,15 @@ on:
|
||||
required: false
|
||||
default: 'jsahn'
|
||||
type: string
|
||||
workspace_id:
|
||||
description: 'AgentBackend workspace_id'
|
||||
workspace_name:
|
||||
description: '작업실 이름 (예: 팬아웃 테스트). GET /workspaces/lookup 으로 UUID 자동 해석'
|
||||
required: false
|
||||
default: 'e1d0baed-cec9-44af-9989-b7aab09bcfd1'
|
||||
default: ''
|
||||
type: string
|
||||
workspace_id:
|
||||
description: '작업실 UUID 직접 지정(override). 비우면 workspace_name 으로 해석'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
user_input:
|
||||
description: 'Agent 에 전달할 user_input'
|
||||
@@ -95,16 +102,18 @@ jobs:
|
||||
YAML_PATH: ${{ inputs.yaml_path }}
|
||||
API_BASE: ${{ inputs.api_base }}
|
||||
AGENT_USER_ID: ${{ inputs.user_id }}
|
||||
AGENT_WORKSPACE_NAME: ${{ inputs.workspace_name }}
|
||||
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
|
||||
AGENT_USER_INPUT: ${{ inputs.user_input }}
|
||||
START_STAGE_INDEX: ${{ inputs.start_stage_index }}
|
||||
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
|
||||
# '=' 형식 필수: user_input 이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
|
||||
# '=' 형식 필수: 값이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
|
||||
run: |
|
||||
python scripts/run_agent_api.py \
|
||||
--yaml-path="$YAML_PATH" \
|
||||
--api-base="$API_BASE" \
|
||||
--user-id="$AGENT_USER_ID" \
|
||||
--workspace-name="$AGENT_WORKSPACE_NAME" \
|
||||
--workspace-id="$AGENT_WORKSPACE_ID" \
|
||||
--user-input="$AGENT_USER_INPUT" \
|
||||
--start-stage-index="$START_STAGE_INDEX" \
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Upload an Agent YAML to AgentBackend and execute it via SSE.
|
||||
|
||||
Flow (SKILL.md §0.6 — 경로 A):
|
||||
0. GET {api_base}/workspaces/lookup?user_id=... (workspace_name → UUID 해석)
|
||||
1. POST {api_base}/upload-agent?user_id=... (multipart YAML 등록)
|
||||
2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
|
||||
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
|
||||
@@ -25,6 +26,7 @@ import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
@@ -58,6 +60,7 @@ class AgentRunner:
|
||||
self.yaml_path = Path(args.yaml_path)
|
||||
self.user_id = args.user_id
|
||||
self.workspace_id = args.workspace_id
|
||||
self.workspace_name = args.workspace_name
|
||||
self.user_input = args.user_input
|
||||
self.start_stage_index = args.start_stage_index
|
||||
self.read_timeout = args.read_timeout
|
||||
@@ -84,6 +87,74 @@ class AgentRunner:
|
||||
def _remaining(self) -> float:
|
||||
return self.deadline - time.monotonic()
|
||||
|
||||
# ------------------------------------------------------------- workspace
|
||||
|
||||
def resolve_workspace(self) -> None:
|
||||
"""workspace_id 를 확정한다.
|
||||
|
||||
- workspace_id 가 주어지면(override) 그대로 사용.
|
||||
- 아니면 workspace_name 을 GET /workspaces/lookup 으로 UUID 해석.
|
||||
둘 다 없으면 SystemExit.
|
||||
"""
|
||||
if self.workspace_id:
|
||||
log(f"Using explicit workspace_id: {self.workspace_id}")
|
||||
return
|
||||
if not self.workspace_name:
|
||||
raise SystemExit("ERROR: workspace_name 또는 workspace_id 중 하나를 지정해야 합니다.")
|
||||
|
||||
log(f"Resolving workspace by name: {self.workspace_name!r} (user_id={self.user_id})")
|
||||
try:
|
||||
resp = self.api_client.get(
|
||||
f"{self.api_base}/workspaces/lookup",
|
||||
params={"user_id": self.user_id},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup 요청 실패: {exc}")
|
||||
if resp.status_code != 200:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup HTTP {resp.status_code}: {resp.text[:500]}")
|
||||
try:
|
||||
items = resp.json().get("workspaces", [])
|
||||
except json.JSONDecodeError:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup 응답이 JSON 이 아님: {resp.text[:300]}")
|
||||
|
||||
def norm(s: object) -> str:
|
||||
return unicodedata.normalize("NFC", (s if isinstance(s, str) else "").strip())
|
||||
|
||||
def field(w: dict, *keys: str) -> str:
|
||||
for k in keys:
|
||||
v = w.get(k)
|
||||
if v:
|
||||
return v
|
||||
return ""
|
||||
|
||||
target = norm(self.workspace_name)
|
||||
names, exact, ci = [], [], []
|
||||
for w in items:
|
||||
name = field(w, "name", "workspace_name", "title")
|
||||
wid = field(w, "workspace_id", "id")
|
||||
names.append(name)
|
||||
if not wid:
|
||||
continue
|
||||
if norm(name) == target:
|
||||
exact.append(wid)
|
||||
elif norm(name).lower() == target.lower():
|
||||
ci.append(wid)
|
||||
|
||||
chosen = exact if exact else ci
|
||||
if len(chosen) == 1:
|
||||
self.workspace_id = chosen[0]
|
||||
log(f"Resolved workspace {self.workspace_name!r} -> {self.workspace_id}")
|
||||
elif len(chosen) > 1:
|
||||
raise SystemExit(
|
||||
f"ERROR: 워크스페이스 이름 {self.workspace_name!r} 이 여러 개 매칭됩니다: {chosen}"
|
||||
)
|
||||
else:
|
||||
available = ", ".join(repr(n) for n in names) or "(없음)"
|
||||
raise SystemExit(
|
||||
f"ERROR: {self.workspace_name!r} 에 해당하는 워크스페이스를 찾지 못했습니다. "
|
||||
f"사용 가능: {available}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ upload
|
||||
|
||||
def upload(self) -> None:
|
||||
@@ -269,6 +340,7 @@ class AgentRunner:
|
||||
|
||||
def run(self) -> int:
|
||||
try:
|
||||
self.resolve_workspace()
|
||||
self.upload()
|
||||
|
||||
mode = "start"
|
||||
@@ -349,7 +421,8 @@ class AgentRunner:
|
||||
f"| Agent | `{self.agent_name or '-'}` |",
|
||||
f"| YAML | `{self.yaml_path}` |",
|
||||
f"| Session | `{self.session_id or '-'}` |",
|
||||
f"| user_id / workspace_id | `{self.user_id}` / `{self.workspace_id}` |",
|
||||
f"| workspace | `{self.workspace_name or '-'}` (`{self.workspace_id or '-'}`) |",
|
||||
f"| user_id | `{self.user_id}` |",
|
||||
f"| 최종 상태 | {status_icon} `{self.final_status}` |",
|
||||
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
|
||||
f"| 확인(confirm)한 stage 수 | {self.stages_confirmed} |",
|
||||
@@ -374,7 +447,10 @@ def parse_args() -> argparse.Namespace:
|
||||
p.add_argument("--yaml-path", required=True, help="실행할 Agent YAML 경로 (repo 상대/절대)")
|
||||
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
|
||||
p.add_argument("--user-id", default="jsahn")
|
||||
p.add_argument("--workspace-id", default="e1d0baed-cec9-44af-9989-b7aab09bcfd1")
|
||||
p.add_argument("--workspace-name", default="",
|
||||
help="작업실 이름. GET /workspaces/lookup 으로 UUID 해석 (workspace-id 미지정 시)")
|
||||
p.add_argument("--workspace-id", default="",
|
||||
help="작업실 UUID 직접 지정(override). 지정 시 workspace-name 무시")
|
||||
p.add_argument("--user-input", default="")
|
||||
p.add_argument("--start-stage-index", type=int, default=0)
|
||||
p.add_argument("--output-dir", default="agent_run_output")
|
||||
|
||||
Reference in New Issue
Block a user