런타임이 채널을 모르므로 채널·챗봇 심사 없이 에이전트 전체를 빌더 화면에서 검증할 수 있다. 웹훅 핸들러 안에 짜면 빌더에서 같은 걸 못 쓰고, 심사가 끝나야 무엇 하나 확인되지 않는다 — 카톡은 나중에 붙는 두 번째 입구다. - services/agent/tools.py: 도구 넷 + 등급 셋(READ·REVERSIBLE·SEMI). ★ 도구는 반드시 services/* 를 통과한다 — crud 를 직접 부르면 스키마 검증· 출처 필수·정정본 보호가 아무 증상 없이 사라진다. 테스트가 소스로 검사한다 - services/agent/runtime.py: 발화 → 도구 선택(LLM 1콜) → 실행 → 응답 - services/prompts/agent.py: LLM 네 겹 규약대로 프롬프트만 여기 - router/v1/agent/chat.py + features/agent/AgentChatDock.tsx(/sites 우하단) 모델에게 맡기지 않은 셋: - 등급 — 응답 스키마에 칸 자체가 없다. 모델이 정하면 프롬프트에 끼어든 한 줄이 확인 절차를 건너뛴다 - 결과 문구 — 도구가 만든다. 모델이 쓰면 하지 않은 일을 했다고 말할 수 있고 사장님에게는 그 말이 사실로 보인다 - key — set_fact 의 key 는 업종 스키마가 최종 판정이다 확인(SEMI)은 실행하지 않고 되묻는다. 돌아온 confirm 값을 믿지 않고 도구는 레지스트리에서 다시 찾고 인자는 도구가 다시 검증한다 — 확인 절차가 검증을 건너뛰는 구멍이 되면 안 된다. 값을 고치면 재발행 안내를 함께 낸다 — fact 는 바뀌어도 사이트는 안 바뀐다. test_agent_runtime.py 17 passed(LLM 은 monkeypatch, 실제 모델 호출 없음). 전체 796 passed / 50 failed — 그 50건은 HEAD 에서도 동일한 기존 이슈. npm run lint 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.2 KiB
Python
69 lines
2.2 KiB
Python
"""사장님 에이전트 대화 — 빌더 화면의 입구.
|
|
|
|
★ 카카오톡 웹훅이 생겨도 이 파일은 안 바뀐다. 런타임이 채널을 모르고, 웹훅은 그저
|
|
같은 `runtime.chat()` 을 부르는 두 번째 입구가 된다(docs/AGENT.md).
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
|
from pydantic import BaseModel, Field
|
|
|
|
from common.models.gmodel import UserInfo
|
|
from router.v1.validator.dependencies import IsValidAccessToken
|
|
from services.agent import runtime
|
|
from services.agent.runtime import AgentError
|
|
|
|
router = APIRouter(prefix="/v1/agent", tags=["Agent"])
|
|
|
|
_STATUS = {
|
|
"PLACE_NOT_FOUND": 404,
|
|
"AGENT_NOT_CONFIGURED": 409,
|
|
"AGENT_UNKNOWN_TOOL": 409,
|
|
"AGENT_EMPTY_MESSAGE": 400,
|
|
"AGENT_MESSAGE_TOO_LONG": 400,
|
|
"AGENT_CALL_FAILED": 502,
|
|
}
|
|
|
|
|
|
class Confirm(BaseModel):
|
|
"""직전 답의 확인 버튼이 그대로 돌려보내는 값.
|
|
|
|
★ 서버는 이 값을 믿지 않는다 — 도구 이름은 레지스트리에서 다시 찾고, 인자는 도구가
|
|
다시 검증한다. 확인 절차가 오히려 검증을 건너뛰는 구멍이 되면 안 된다."""
|
|
|
|
tool: str = Field(min_length=1, max_length=40)
|
|
args: dict = {}
|
|
|
|
|
|
class Req_Chat(BaseModel):
|
|
message: str = Field(default="", max_length=runtime.MAX_MESSAGE)
|
|
confirm: Confirm | None = None
|
|
|
|
|
|
@router.get("/status")
|
|
async def status(response: Response, user: UserInfo = Depends(IsValidAccessToken)):
|
|
"""대화창을 열 수 있는지. 키가 없으면 화면은 자리를 두고 입력만 죽인다."""
|
|
response.headers["Cache-Control"] = "no-store"
|
|
return {"enabled": runtime.is_configured()}
|
|
|
|
|
|
@router.post("/chat/{place_id}")
|
|
async def chat(
|
|
place_id: UUID,
|
|
req: Req_Chat,
|
|
response: Response,
|
|
user: UserInfo = Depends(IsValidAccessToken),
|
|
):
|
|
response.headers["Cache-Control"] = "no-store"
|
|
response.headers["Referrer-Policy"] = "no-referrer"
|
|
try:
|
|
return await runtime.chat(
|
|
user,
|
|
str(place_id),
|
|
req.message,
|
|
confirm=req.confirm.model_dump() if req.confirm else None,
|
|
)
|
|
except AgentError as ex:
|
|
raise HTTPException(_STATUS.get(str(ex), 409), str(ex)) from ex
|