Ask User

Ask User

모델이 실행 중 다지선다 질문을 사용자에게 묻고 답을 기다리게 해요.

Source

Pydantic AI Harness가 0.x 릴리스인 동안 API는 마이너 릴리스 사이에 바뀔 수 있어요. 바뀔 때는 폐기 경고와 릴리스 노트 마이그레이션 안내가 정확히 어떻게 업그레이드할지 알려줘요. 버전 정책 참고.

출처: 문서

본문

문제 (The problem)

모호한 작업을 받은 에이전트는 추측하거나, 질문을 출력에 묻고 멈춰요. 추측은 실행을 낭비하고, 산문 속 질문은 전체 답변을 읽는 사람만 발견해요. 모델은 작고 구조화된 질문을 하고 답을 데이터로 받아올 방법이 필요해요.

해결책 (The solution)

AskUser는 도구 하나, ask_user_question을 노출해요. 모델은 질문 110개를 넘기고, 각각 짧은 header, 질문 텍스트, 옵션 26개(label과 선택적 description), 그리고 여러 답이 허용될 때 multi_select를 담아요. capability가 호출을 검증하고, 그것을 당신의 answerer에 넘기며, 선택된 라벨을 header별로 키로 반환해요. 사용자가 거절하면 모델에게 그렇게 알리고 실행은 계속돼요.

capability가 스키마와 검증을 소유해요. 절대 출력하지 않고, stdin을 읽지 않으며, 터미널 라이브러리를 임포트하지 않아요. answerer가 무엇이든 질문을 사람 앞에 놓는 것이고, 당신이 공급해요. 기본값은 없어요. stdin을 읽는 capability는 서버에서 쓸 수 없기 때문이에요.

from pydantic_ai import Agent
from pydantic_ai_harness import AskUser
from pydantic_ai_harness.ask_user import AskUserAnswer, AskUserRequest, AskUserResponse


async def pick_first(request: AskUserRequest) -> AskUserResponse:
    answers = [AskUserAnswer(header=q.header, selected=(q.options[0].label,)) for q in request.questions]
    return AskUserResponse(answers=tuple(answers))


agent = Agent('anthropic:claude-fable-5', capabilities=[AskUser(answerer=pick_first)])

pick_first는 실제 UI를 대신해요. CLAI가 같은 프로토콜 위에 지어진 터미널 메뉴를 선적하고, 웹 폼도 또 하나가 될 수 있어요.

answerer 작성하기 (Writing an answerer)

Answerer는 하나의 비동기 호출 가능이에요. AskUserRequest를 받고 AskUserResponse를 반환합니다. 평범한 async def도 자격이 되고, async def __call__이 있는 클래스도 마찬가지예요.

  • AskUserRequest.questions는 검증된 Question 객체를 담고, AskUserRequest.id는 동시 또는 반복 호출을 구분해 UI가 자기 회신을 요청과 매칭하게 해요.
  • 완료된 AskUserResponse는 질문당 AskUserAnswer 하나를 header로 키해 담고, 선택된 옵션 라벨이 selected에 있어요. 질문이 multi_select가 아니면 정확히 하나, 어느 쪽이든 최소 하나, 라벨이 두 번 없어요.
  • answerer는 대신 AskUserAnswer(header=q.header, custom_answer='My own answer')를 반환할 수도 있어요. selected는 비워 두세요. 커스텀 텍스트는 비어 있지 않고, 줄바꿈 외의 제어 문자가 없어야 해요. 도구는 그것을 같은 header 아래의 한 항목 리스트로 반환해요. 기존 선택-라벨 응답은 형식을 유지해요. 텍스트 입력을 제공할지 여부는 모델의 질문 스키마가 아니라 answerer가 결정해요.
  • 사용자가 거절하면 답 없이 AskUserResponse(cancelled=True)를 반환하세요. 도구 결과는 모델에게 사용자가 거절했다고 알려요. 실행에 아무것도 발생시키지 않습니다.
  • 요청에 맞지 않는 응답(알 수 없는 header, 질문이 제공하지 않은 라벨, 단일 선택 질문의 여러 라벨, 누락된 답)은 answerer의 버그이고 ValueError를 발생시켜 실행이 실패해요. answerer가 반환 전에 검증할 수 있도록 check_response가 내보내져 있어요.

실행은 도구 호출 안에서 answerer가 반환할 때까지 기다려요. 그래서 다른 요청에서 답을 수집하는 웹 프론트엔드는 답이 도착할 때까지 실행을 열어 두어야 해요.

답하지 않고 관찰하기 (Watching without answering)

CapabilityEvent가 실행의 다른 것이 교환을 관찰하게 해요:

이벤트 시점 필드
AskUserRequestedEvent answerer가 호출되기 전 request
AskUserAnsweredEvent 반환 후, 응답이 검사되거나 모델이 결과를 보기 전 request_id, response

둘 다 즉시 디스패치돼요. "당신을 기다리는 중" 상태를 보여주는 리스너가 실행과 보조를 맞춰 대기의 시작과 끝을 보게요. capability에서 @on_event로, 또는 실행의 이벤트 스트림을 통해 구독하세요:

from pydantic_ai import RunContext
from pydantic_ai.capabilities import AbstractCapability, on_event

from pydantic_ai_harness.ask_user import AskUserAnsweredEvent, AskUserRequestedEvent


class WaitIndicator(AbstractCapability[None]):
    @on_event(AskUserRequestedEvent)
    async def waiting(self, ctx: RunContext[None], event: AskUserRequestedEvent) -> None:
        print(f'waiting on {len(event.request.questions)} question(s)')

    @on_event(AskUserAnsweredEvent)
    async def done(self, ctx: RunContext[None], event: AskUserAnsweredEvent) -> None:
        print('declined' if event.response.cancelled else 'answered')

모델이 보는 것 (What the model sees)

도구 스키마는 Code Puppy의 ask_user_question을 거울로 반영해서, 그것을 위해 쓰인 프롬프트가 그대로 이어져요. 한계: 호출당 질문 110개, 최대 25자 고유 header, 최대 500자 질문 텍스트, 질문당 옵션 26개(최대 50자 고유 라벨과 최대 200자 description), 어디에도 제어 문자가 없어요(header와 라벨은 한 줄, 질문 텍스트와 description은 여러 줄 가능). 이 문자열들은 터미널에 그려지고, 프롬프트 주입된 호출의 이스케이프 시퀀스는 공격이니까요. 그 한계 밖의 호출이나 스키마에 없는 필드를 가진 호출은 answerer에 보내지 않고 검증 재시도로 모델에 반환돼요. 검증되면 질문은 고정돼요. answerer가 보는 것은 모델이 물은 것과 같아요.

결과는 각 header를 선택된 라벨 목록(또는 커스텀 답 하나)에 매핑하는 JSON 객체이거나, 문장 The user declined to answer. Continue without the answer, or ask differently if it is essential.이에요.

capability는 지시문 하나를 추가해요. 작업이 모호하고 답이 워크스페이스에 없을 때 묻고, 구체적인 옵션을 제공하고, 관련 질문을 묶고, 사용자가 거절하면 명시된 선택을 해요.

그것이 둘 (Two of them)

AskUser는 기본 id를 선언하지 않아요. 하나의 에이전트에 둘이면 ask_user_question 도구 이름에서 충돌해요. answerer 둘은 설정을 두 번 말하는 것이 아니라 충돌이에요.

추적 (Tracing)

AskUser는 스팬을 방출하지 않아요. Core의 도구 호출 스팬이 이미 대기를 다루고, 위의 두 이벤트가 무엇을 물었고 답했는지를 담아요.

스펙 (Specs)

Agent.from_specAskUser를 구성할 수 없어요. answerer는 스펙이 이름 지을 방법이 없는 라이브 객체니까요.

API 참고 (API reference)

check_response, TOOL_NAME, DECLINED, MAX_QUESTIONSpydantic_ai_harness.ask_user에서 내보내져요.

AskUser

Bases: AbstractCapability[AgentDepsT]

모델이 실행 중 다지선다 질문을 사용자에게 묻게 해요.

capability가 질문 스키마와 검증을 소유하고, answerer가 사람을 소유해요. 터미널 메뉴든 웹 폼이든 테스트의 스크립트 함수든, 실행은 도구 호출 안에서 그것을 기다리고 모델은 선택된 라벨을 돌려받아요. 기본값은 없어요. stdin을 읽는 capability는 서버에서 쓸 수 없으니까요.

from pydantic_ai import Agent
from pydantic_ai_harness import AskUser
from pydantic_ai_harness.ask_user import AskUserAnswer, AskUserRequest, AskUserResponse


async def pick_first(request: AskUserRequest) -> AskUserResponse:
    answers = [AskUserAnswer(header=q.header, selected=(q.options[0].label,)) for q in request.questions]
    return AskUserResponse(answers=tuple(answers))


agent = Agent('anthropic:claude-fable-5', capabilities=[AskUser(answerer=pick_first)])
속성 (Attributes)
answerer

AskUserRequest를 사용자에게 제시하고 그들의 AskUserResponse를 반환해요.

타입: Answerer

메서드 (Methods)
get_instructions
def get_instructions() -> AgentInstructions[AgentDepsT] | None

언제 물을지에 대한 정적이고 캐시 안정적인 안내.

반환

AgentInstructions[AgentDepsT] | None

get_toolset
def get_toolset() -> AgentToolset[AgentDepsT] | None

이 capability의 answerer에 바인딩된 ask_user_question 도구.

반환

AgentToolset[AgentDepsT] | None

Answerer

Bases: Protocol

질문을 사람 앞에 놓는 무엇이든: 터미널 메뉴, 웹 폼, 스크립트 테스트.

이 시그니처가 있는 평범한 async def가 충족해요. 사용자가 거절하면 취소 응답을 반환하고, 실행을 실패시켜야 하는 실패에만 발생시키세요.

메서드 (Methods)
call

@async

def __call__(request: AskUserRequest, /) -> AskUserResponse

request를 사용자에게 제시하고 그들이 고른 것을 반환해요.

반환

AskUserResponse

AskUserRequest

ask_user_question 호출 하나. Answerer에 넘겨지고 AskUserRequestedEvent가 나른다.

속성 (Attributes)
id

동시 또는 반복 호출을 구분해요. UI가 회신을 그것에 매칭해요.

타입: str 기본: field(default_factory=(lambda: uuid4().hex))

AskUserResponse

Answerer가 반환하는 것: 질문당 답 하나, 또는 사용자가 거절하면 cancelled.

AskUserAnswer

한 질문에 대해 선택된 라벨, 또는 그 라벨 대신 커스텀 답.

Question

Bases: BaseModel

다지선다 질문 하나.

QuestionOption

Bases: BaseModel

사용자가 고를 수 있는 선택지 하나.

AskUserRequestedEvent

Bases: CapabilityEvent

모델이 사용자에게 무언가를 물었고, answerer가 곧 호출될 거예요.

AskUserAnsweredEvent

Bases: CapabilityEvent

answerer가 반환했고, response.cancelled가 사용자가 거절했는지 말해줘요.

응답이 요청에 대해 검사되기 전에 방출되므로, 요청 이벤트부터 기다려 온 리스너는 answerer가 잘못 행동하고 실행이 곧 실패할 때에도 해제돼요.

더 알아보기 (Learn more)