본문으로 건너뛰기

prompter

prompter는 XML 프롬프트 계약을 기준으로 LLM 요청을 실행하는 모듈입니다. OpenAI, Claude, Gemini, Ollama, LM Studio 같은 제공자를 LLMSource로 등록하고, transact에서는 CommandType=P로 라우팅합니다.

책임 범위​

  • 프롬프트 계약을 읽어 provider, model, 메시지, body, tool 사용 범위를 구성합니다.
  • 요청 파라미터를 프롬프트 본문과 헤더, 인증 정보, body 속성에 바인딩합니다.
  • KernelPlugin, MCP 서버, CLI 도구, body 파일 경로 사용을 allowlist로 제한합니다.
  • LLM 응답을 HandStack 거래 응답 형식으로 변환합니다.
  • 요청/응답 로그와 채팅 이력 출력 여부를 설정으로 제어합니다.

주요 API​

메서드경로용도
GET/prompter/api/query/has프롬프트 계약 존재 여부를 확인합니다.
GET/prompter/api/query/refresh프롬프트 계약 캐시를 갱신합니다.
GET/prompter/api/query/retrieve계약 원문 또는 상세 정보를 조회합니다.
GET/prompter/api/query/meta입력/출력 메타 정보를 반환합니다.
GET/prompter/api/query/reports계약 기반 보고용 정보를 조회합니다.
POST/prompter/api/query프롬프트 계약을 실행합니다.

has, refresh, retrieve, meta의 파라미터와 인증 규칙(실패 시 400)은 transact 모듈의 계약 관리 API 공통 규칙을 보세요.

핵심 구현​

  • Areas/prompter/Controllers/QueryController.cs: query API 진입점입니다.
  • DataClient/PromptClient.cs: 프롬프트 계약 실행과 LLM provider 호출을 담당합니다.
  • Extensions/PromptMapper.cs: XML 계약 로드와 refresh를 담당합니다.
  • Entity/ModuleConfigJson.cs: LLMSource와 도구 allowlist 설정 스키마입니다.
  • Events/PrompterRequestHandler.cs: 모듈 내부 MediatR 실행 요청(prompter.Events.PrompterRequest)을 처리합니다.

계약과 식별자​

계약은 기본적으로 다음 위치에 둡니다.

contracts/prompter/{ApplicationID}/{ProjectID}/{TransactionID}.xml

프롬프트 ID는 다음 형식으로 조회됩니다.

ApplicationID|ProjectID|TransactionID|PromptID

프롬프트 본문은 ${ParameterName} 형태로 요청 값을 치환합니다. authorization, headers, body 속성처럼 값 전체를 파라미터로 받을 때는 @ParameterName 형식을 사용합니다. statement의 role 기본값은 user이며, 시스템 지시문으로 전달할 계약만 role="system"을 명시합니다. <message>의 role을 생략하면 statement role을 상속합니다.

Jev·Ollama 판단 계약​

<decision> statement는 TypeSafe Jev 또는 로컬 Ollama의 /v1/systemone을 호출합니다. 기존 CommandType=P와 QueryID 형식을 사용하며, <decision>이 없는 Ollama 채팅 계약은 /api/chat을 유지합니다.

<statement id="GP02" seq="0" use="Y" datasource="DECISION_NIMBLE" timeout="60">
<decision state="@UserMessage"><![CDATA[
{"refund":{"type":"noul","instructions":"Does the customer request a refund?"}}
]]></decision>
<param id="@UserMessage" type="String" length="-1" required="Y" value="" />
<outputmeta value="PromptResult:string" />
</statement>

state는 선언된 입력 파라미터를 참조합니다. 입력은 독립적인 JSON 문자열로 직렬화되며, 질문 기준인 CDATA에는 문자열 치환을 적용하지 않습니다. 질문은 Choice(226개 선택지), Score(210개 순서 있는 기준), Noul(참 확률)을 지원합니다. Noul에는 별도 confidence가 없습니다. 판단 입력은 텍스트이며, 이미지는 <media type="Image">로 함께 첨부할 수 있습니다(이미지 판단). message/tools/body/headers/authorization과는 함께 선언할 수 없습니다. Think/Stream은 false여야 합니다. timeout은 1~300초이며 0 또는 생략은 30초입니다.

ServiceID데이터 소스LLMProviderModelID
GP01DECISION_JEVTypeSafejev-latest
GP02DECISION_NIMBLEOllamanimble
GP03DECISION_CLEF_FLASHOllamaclef-flash
GP04DECISION_CLEFOllamaclef
GP05DECISION_TEV1Ollamatev1
GP06DECISION_CLEF_FLASHOllamaclef-flash (이미지 판단)
GP07DECISION_JEVTypeSafejev-latest (이미지 판단)

소스 module.json에는 위 데이터 소스가 포함되어 있습니다. 실제 실행 모듈 설정에도 해당 항목을 병합합니다. TypeSafe의 Endpoint는 https://api.typesafe.ai이며 ApiKey가 비어 있으면 호스트 프로세스의 TYPESAFE_API_KEY를 사용합니다. 기존 ApiKey 암호화 설정도 사용할 수 있습니다. Ollama 판단의 기본 Endpoint는 http://127.0.0.1:11434이며 루프백 주소만 허용하고 Authorization 헤더를 보내지 않습니다. 허용 모델은 ModuleConfig.AllowedOllamaDecisionModels 배열에서 관리합니다. 배포 설정에는 네 모델의 기본/:latest 태그와 tev1:4b, tev1:0.8b가 등록되어 있습니다. 새 모델은 허용 목록과 LLMSource.ModelID에 같은 값을 지정하면 코드 수정 없이 사용할 수 있습니다. /v1/systemone을 실제 지원하는 로컬 모델과 런타임을 호스트 컴퓨터에 준비해야 합니다.

{
"AllowedOllamaDecisionModels": ["nimble", "nimble:latest", "tev1:4b"]
}

위 설정은 ModuleConfig 안에 넣습니다. 모델명과 태그는 대소문자를 구분해 전체 문자열이 일치해야 하며 와일드카드는 지원하지 않습니다. 앞뒤 공백·중복·빈 항목은 정리합니다. 목록 누락, null 또는 빈 배열은 Ollama 판단 호출을 차단하며 코드의 고정 목록으로 대체하지 않습니다. 기존 설정에는 이 새 항목도 병합해야 합니다. TypeSafe 판단과 일반 Ollama 채팅은 영향을 받지 않습니다. 호스트의 AppSettings:IsConfigurationWatching=true이면 변경 후 시작하는 모델 검사부터 새 목록을 적용합니다. 꺼져 있으면 호스트를 다시 시작합니다. 모듈 설정 화면의 AllowedOllamaDecisionModels 입력란에서도 목록을 편집할 수 있습니다.

전체 예제는 JEV010.xml, 거래 계약, 직접 호출 요청, 이미지 첨부 요청, 데이터 소스 설정입니다. XML은 contracts/prompter/HDS/LLM/JEV010.xml, 거래 JSON은 contracts/transact/HDS/LLM/JEV010.json에 배치합니다. 모듈 Contracts에도 최신본을 반영하고 호스트를 다시 시작합니다. 기본 요청은 HDS|LLM|JEV010|GP0200을 실행합니다.

직접 API에서는 Acknowledge=1과 빈 ExceptionText를 확인한 다음 ResultJson[0].Value.PromptResult를 JSON으로 역직렬화합니다. Form 행의 ID는 FormData0입니다. transact 경로에서는 acknowledge와 result.dataSet[0].value.PromptResult를 확인합니다. PromptResult는 검증된 전체 응답 문자열이며 outputmeta가 개별 JSON 경로를 숫자 필드에 매핑하지는 않습니다.

확률 범위·합, 선택지, Score 가중합 등을 검증하고 429·500·타임아웃·잘못된 응답은 거래 실패로 처리합니다. 임의 결과나 채팅으로 대체하지 않습니다. 사람 검토 정책과 입력 마스킹은 호출 앱이 담당합니다. prompter의 요청 로그는 decision state를 가리지만 transact·프록시 로그에도 필요한 보호 정책을 적용해야 합니다. 실제 모델 품질 검증과 별개로, 개발 검증에서는 실제 ack/transact/prompter를 외부 제공자의 로컬 Postman 모의 서버에 연결해 성공·실패 경로를 확인했습니다.

공식 형식: TypeSafe Quick Start, Ollama Decision.

이미지 판단​

<decision> statement에 <media type="Image">를 함께 선언하면 첨부 이미지를 모든 질문이 공유하는 입력으로 판단합니다. 샘플 JEV010.xml의 GP06(Ollama clef-flash)과 GP07(TypeSafe jev-latest)이 예시이며, @Image1(image/png)과 @Image2(image/jpeg)를 선택 입력으로 받습니다. 값은 data: URI가 아닌 Base64 원문이고, 이미지가 없으면 텍스트만으로 판단합니다. media는 Image 형식만 최대 4개까지 선언할 수 있습니다.

<statement id="GP06" seq="0" use="Y" datasource="DECISION_CLEF_FLASH" timeout="120">
<decision state="@UserMessage"><![CDATA[ ... ]]></decision>
<param id="@UserMessage" type="String" length="-1" required="Y" value="" />
<media id="@Image1" type="Image" mimeType="image/png" required="N" />
<media id="@Image2" type="Image" mimeType="image/jpeg" required="N" />
<outputmeta value="PromptResult:string" />
</statement>
항목Ollama (/v1/systemone)TypeSafe
전송 형식본문 images: ["<Base64>"]본문 assets 맵과 state 끝의 참조 키(요청마다 만든 UUID), TypeSafe-Danger-Zone-Images: true 헤더 자동 추가
허용 형식PNG, JPEG, WebP애니메이션이 아닌 PNG, JPEG
크기요청 전체 32 MiB 이하파일 16 MiB 이하, 가로·세로 280~1568px, 요청 전체 32 MiB 이하
지원 모델AllowedOllamaImageDecisionModels에 등록한 모델(기본 clef, clef-flash, Ollama 0.35.1 이상)Jev 1.13 이상
  • AllowedOllamaImageDecisionModels는 AllowedOllamaDecisionModels와 같은 방식(정확한 문자열 비교, 모듈 설정 화면 편집, 실행 중 교체)으로 관리합니다. 이 목록에 없는 모델(nimble, tev1 등 텍스트 전용)에 이미지를 보내면 호출 전에 decision_image_model_unsupported로 실패합니다. 이미지를 무시한 판단이 정상 결과처럼 반환되는 일을 막기 위한 차단입니다.
  • 실제 형식과 크기는 파일 헤더로 호출 전에 검사합니다. 선언한 mimeType과 실제 형식이 다르거나 손상된 이미지는 decision_image_invalid, TypeSafe 형식 제한은 decision_image_format_unsupported, 크기 제한은 decision_image_size_unsupported, 요청 전체 32 MiB 초과는 decision_image_too_large입니다.
  • TypeSafe는 이미지 메타데이터(EXIF 포함)를 제거하고 회전을 적용하지 않으므로 이미지를 바로 세워서 보내고, GIF·HEIC는 PNG나 JPEG로 변환합니다. 이 기능은 공급자가 아직 production-ready가 아닌 기능으로 표시하며 형식과 한도가 바뀔 수 있습니다.
  • 이미지에는 패턴 마스킹을 적용할 수 없습니다. 개인정보가 담길 수 있는 이미지는 외부로 전송되는 TypeSafe(GP07) 대신 로컬 Ollama(GP06)로 판단합니다. prompter의 요청 로그는 이미지 값을 [media:Image;base64-length:N]로 가립니다.
  • 직접 호출은 이미지 파라미터를 추가합니다: { "ParameterName": "Image1", "Value": "<Base64 원문>", "DbType": "String", "Length": -1 }.

공식 형식: TypeSafe Images, Ollama Decision.

이미지·오디오 입력​

계약의 <media>는 QueryObject.Parameters에 담긴 Base64 원문을 텍스트와 분리하여 provider별 멀티모달 메시지로 전달합니다.

<statement id="GP02" seq="0" role="system">
<message role="user"><![CDATA[
${UserMessage}
]]></message>
<param id="@UserMessage" type="String" length="-1" value="" />
<media id="@Image1" type="Image" mimeType="image/png" required="N" />
<media id="@Audio1" type="Audio" mimeType="audio/wav" required="N" />
</statement>
  • id는 <param>과 중복될 수 없으며 대소문자를 구분하지 않습니다.
  • type은 Image, Audio만 허용합니다.
  • mimeType은 image/png, image/jpeg, audio/wav, audio/mpeg 같은 구체적인 값이어야 하며 wildcard는 허용하지 않습니다.
  • required="Y"인 값이 없거나 값이 유효한 Base64가 아니면 provider 호출 전에 실패합니다. required="N"의 빈 값은 생략합니다.
  • 파라미터 값에는 URL이나 data:...;base64, 접두사를 넣지 않고 Base64 데이터만 넣습니다.
  • media 파라미터는 프롬프트, authorization, headers, body, pretransaction 텍스트 치환에서 제외되고 현재 statement가 만든 마지막 user 메시지에 첨부됩니다. user 메시지가 없으면 media 전용 메시지를 만듭니다.
LLMProviderImageAudioprovider 요청 형식
OpenAI지원지원 (audio/wav, audio/mpeg)Chat Completions content parts
Claude지원미지원Messages base64 image source
Gemini지원지원GenerateContent inlineData
Ollama지원미지원Chat message images
LM Studio지원미지원OpenAI 호환 image_url content part

provider뿐 아니라 선택한 모델도 해당 modality를 지원해야 합니다. 미지원 media는 네트워크 요청 전에 오류로 처리됩니다. prompter 내부 로그는 media 값을 [media:유형;base64-length:길이]로 마스킹하지만, 앞단 transact와 프록시 로그에도 원문을 저장하지 않도록 별도 정책을 적용해야 합니다.

실행 가능한 예시는 CLS010.xml과 request.json에서 확인할 수 있습니다. 계약을 contracts/prompter/HDS/LLM/CLS010.xml에 배치하고 LLM1을 OpenAI 또는 Gemini의 멀티모달 모델로 설정한 뒤 request.json을 /prompter/api/query에 POST합니다.

주요 설정​

설정설명
LLMSourceprovider, model, endpoint, API Key, stream/think 옵션을 정의합니다.
AllowedOllamaDecisionModels로컬 Ollama 판단 계약에서 허용할 모델명·태그의 정확한 목록입니다. 미설정·빈 목록은 판단 호출을 차단합니다.
AllowedOllamaImageDecisionModels위 목록 중 이미지 입력을 지원하는 모델(기본 clef, clef-flash와 :latest)의 목록입니다. 목록에 없는 모델에 이미지를 보내면 호출 전에 차단합니다.
AllowedKernelPlugins계약에서 호출 가능한 커널 플러그인과 함수 allowlist입니다.
AllowedMcpServers계약에서 사용할 수 있는 MCP 서버 allowlist입니다.
AllowedCliTools프롬프트 실행 중 사용할 수 있는 CLI 도구 allowlist입니다.
AllowedBodyFileBasePathsbody 파일을 읽을 수 있는 기준 경로입니다.
IsChatHistoryConsoleShow채팅 이력을 콘솔에 출력할지 결정합니다.
EventAction기본값은 prompter.Events.ManagedRequest입니다.

LLMSource 예시는 다음과 같습니다.

{
"ApplicationID": "HDS",
"ProjectID": "*",
"DataSourceID": "LLM1",
"LLMProvider": "OpenAI",
"ApiKey": "<api-key>",
"ModelID": "gpt-5.4-mini",
"Endpoint": "",
"Think": false,
"Stream": false
}

ack 또는 rdy의 AppSettings:IsConfigurationWatching이 true이면 실행 중 실제로 로드된 module.json의 LLMSource 변경을 감지해 PromptMapper.DataSourceMappings를 다시 구성합니다. 새 목록 전체가 유효할 때만 기존 캐시를 교체하며, provider 변환이나 API Key 복호화 오류가 있으면 이전 설정과 캐시를 유지합니다. 교체 완료 후 시작하는 요청부터 새 LLM 설정을 사용하고 빈 배열은 캐시 삭제로 반영됩니다.

LLMProvider 별로 사용 가능한 ModelID 목록​

transact 연동​

transact 거래 계약에서는 CommandType을 P로 둡니다.

{
"ServiceID": "GPT010",
"CommandType": "P",
"ReturnType": "Json"
}

라우팅 예시는 다음과 같습니다.

{
"HDS|*|P|D": "http://localhost:8421/prompter/api/query",
"HDS|*|P|P": "http://localhost:8421/prompter/api/query",
"HDS|*|P|T": "http://localhost:8421/prompter/api/query"
}

운영 주의사항​

  • API Key, endpoint, 로컬 모델 경로는 실제 값으로 계약이나 공개 문서에 남기지 않습니다.
  • Ollama의 일부 qwen3 계열 모델은 비어 있지 않은 user role이 없는 요청을 거부합니다. prompter는 <message>가 없는 기존 system-only 계약을 Ollama로 호출할 때 마지막 유효한 system 메시지를 user로 정규화하며, 메시지 본문은 복제하지 않습니다.
  • 도구 호출은 계약 선언과 module.json allowlist가 모두 일치할 때만 허용되도록 관리합니다.
  • MCP, CLI, 파일 body 기능은 외부 시스템이나 파일 시스템에 접근할 수 있으므로 운영 환경에서는 최소 권한 원칙을 적용합니다.
  • LLM 응답은 비결정적일 수 있으므로 업무 거래에서는 후속 검증, 스키마 검사, 실패 시 fallback 전략을 함께 설계합니다.