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).
|
# 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000).
|
||||||
#
|
#
|
||||||
# 실행 흐름 (SKILL.md §0.6 경로 A):
|
# 실행 흐름 (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 하고 실패 처리한다.
|
# stage_error 발생 시 세션을 cancel 하고 실패 처리한다.
|
||||||
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
|
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
|
||||||
|
# ※ workspace_name(작업실 이름) 또는 workspace_id(UUID) 중 하나는 반드시 지정.
|
||||||
#
|
#
|
||||||
# 실행 시간 상한 (주의):
|
# 실행 시간 상한 (주의):
|
||||||
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
|
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
|
||||||
@@ -48,10 +50,15 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: 'jsahn'
|
default: 'jsahn'
|
||||||
type: string
|
type: string
|
||||||
workspace_id:
|
workspace_name:
|
||||||
description: 'AgentBackend workspace_id'
|
description: '작업실 이름 (예: 팬아웃 테스트). GET /workspaces/lookup 으로 UUID 자동 해석'
|
||||||
required: false
|
required: false
|
||||||
default: 'e1d0baed-cec9-44af-9989-b7aab09bcfd1'
|
default: ''
|
||||||
|
type: string
|
||||||
|
workspace_id:
|
||||||
|
description: '작업실 UUID 직접 지정(override). 비우면 workspace_name 으로 해석'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
type: string
|
type: string
|
||||||
user_input:
|
user_input:
|
||||||
description: 'Agent 에 전달할 user_input'
|
description: 'Agent 에 전달할 user_input'
|
||||||
@@ -95,16 +102,18 @@ jobs:
|
|||||||
YAML_PATH: ${{ inputs.yaml_path }}
|
YAML_PATH: ${{ inputs.yaml_path }}
|
||||||
API_BASE: ${{ inputs.api_base }}
|
API_BASE: ${{ inputs.api_base }}
|
||||||
AGENT_USER_ID: ${{ inputs.user_id }}
|
AGENT_USER_ID: ${{ inputs.user_id }}
|
||||||
|
AGENT_WORKSPACE_NAME: ${{ inputs.workspace_name }}
|
||||||
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
|
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
|
||||||
AGENT_USER_INPUT: ${{ inputs.user_input }}
|
AGENT_USER_INPUT: ${{ inputs.user_input }}
|
||||||
START_STAGE_INDEX: ${{ inputs.start_stage_index }}
|
START_STAGE_INDEX: ${{ inputs.start_stage_index }}
|
||||||
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
|
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
|
||||||
# '=' 형식 필수: user_input 이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
|
# '=' 형식 필수: 값이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
|
||||||
run: |
|
run: |
|
||||||
python scripts/run_agent_api.py \
|
python scripts/run_agent_api.py \
|
||||||
--yaml-path="$YAML_PATH" \
|
--yaml-path="$YAML_PATH" \
|
||||||
--api-base="$API_BASE" \
|
--api-base="$API_BASE" \
|
||||||
--user-id="$AGENT_USER_ID" \
|
--user-id="$AGENT_USER_ID" \
|
||||||
|
--workspace-name="$AGENT_WORKSPACE_NAME" \
|
||||||
--workspace-id="$AGENT_WORKSPACE_ID" \
|
--workspace-id="$AGENT_WORKSPACE_ID" \
|
||||||
--user-input="$AGENT_USER_INPUT" \
|
--user-input="$AGENT_USER_INPUT" \
|
||||||
--start-stage-index="$START_STAGE_INDEX" \
|
--start-stage-index="$START_STAGE_INDEX" \
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Upload an Agent YAML to AgentBackend and execute it via SSE.
|
Upload an Agent YAML to AgentBackend and execute it via SSE.
|
||||||
|
|
||||||
Flow (SKILL.md §0.6 — 경로 A):
|
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 등록)
|
1. POST {api_base}/upload-agent?user_id=... (multipart YAML 등록)
|
||||||
2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
|
2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
|
||||||
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
|
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
|
||||||
@@ -25,6 +26,7 @@ import os
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import unicodedata
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -58,6 +60,7 @@ class AgentRunner:
|
|||||||
self.yaml_path = Path(args.yaml_path)
|
self.yaml_path = Path(args.yaml_path)
|
||||||
self.user_id = args.user_id
|
self.user_id = args.user_id
|
||||||
self.workspace_id = args.workspace_id
|
self.workspace_id = args.workspace_id
|
||||||
|
self.workspace_name = args.workspace_name
|
||||||
self.user_input = args.user_input
|
self.user_input = args.user_input
|
||||||
self.start_stage_index = args.start_stage_index
|
self.start_stage_index = args.start_stage_index
|
||||||
self.read_timeout = args.read_timeout
|
self.read_timeout = args.read_timeout
|
||||||
@@ -84,6 +87,74 @@ class AgentRunner:
|
|||||||
def _remaining(self) -> float:
|
def _remaining(self) -> float:
|
||||||
return self.deadline - time.monotonic()
|
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
|
# ------------------------------------------------------------------ upload
|
||||||
|
|
||||||
def upload(self) -> None:
|
def upload(self) -> None:
|
||||||
@@ -269,6 +340,7 @@ class AgentRunner:
|
|||||||
|
|
||||||
def run(self) -> int:
|
def run(self) -> int:
|
||||||
try:
|
try:
|
||||||
|
self.resolve_workspace()
|
||||||
self.upload()
|
self.upload()
|
||||||
|
|
||||||
mode = "start"
|
mode = "start"
|
||||||
@@ -349,7 +421,8 @@ class AgentRunner:
|
|||||||
f"| Agent | `{self.agent_name or '-'}` |",
|
f"| Agent | `{self.agent_name or '-'}` |",
|
||||||
f"| YAML | `{self.yaml_path}` |",
|
f"| YAML | `{self.yaml_path}` |",
|
||||||
f"| Session | `{self.session_id or '-'}` |",
|
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"| 최종 상태 | {status_icon} `{self.final_status}` |",
|
||||||
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
|
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
|
||||||
f"| 확인(confirm)한 stage 수 | {self.stages_confirmed} |",
|
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("--yaml-path", required=True, help="실행할 Agent YAML 경로 (repo 상대/절대)")
|
||||||
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
|
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
|
||||||
p.add_argument("--user-id", default="jsahn")
|
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("--user-input", default="")
|
||||||
p.add_argument("--start-stage-index", type=int, default=0)
|
p.add_argument("--start-stage-index", type=int, default=0)
|
||||||
p.add_argument("--output-dir", default="agent_run_output")
|
p.add_argument("--output-dir", default="agent_run_output")
|
||||||
|
|||||||
Reference in New Issue
Block a user