refactor: 이드 페르소나 프롬프트를 intent_service.py 단일 소스로 통합
- intent_service.py: PERSONA_FULL/PERSONA_LOCAL 상수 정의 + GET /persona 엔드포인트 추가 - 기존 ID_SYSTEM_PROMPT (반말) 제거, PERSONA_LOCAL (존댓말)로 교체 - [자아], [기능 범위] 섹션 추가로 Opus 거짓 응답 방지 - n8n: Call Qwen/Haiku/Opus 3개 노드 → /persona 엔드포인트 런타임 fetch로 전환 - 각 노드에 httpGet 함수 + fallback 프롬프트 추가 - Haiku의 [기능 범위] 별도 추가 코드 제거 (PERSONA_FULL에 이미 포함) - docker-compose.yml: INTENT_SERVICE_URL 환경변수 추가 - manage_services.sh: intent-service를 SERVICES 배열에 추가 - init/migrate-v8.sql: DB ai_configs/prompts 표시용 동기화 - init/init.sql: 시드 데이터 동기화 + "표시용" 주석 추가 - docs/architecture.md: 페르소나 섹션 업데이트 (단일 소스 명시) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ services:
|
|||||||
- CALDAV_BRIDGE_URL=http://host.docker.internal:8092
|
- CALDAV_BRIDGE_URL=http://host.docker.internal:8092
|
||||||
- MAIL_BRIDGE_URL=http://host.docker.internal:8094
|
- MAIL_BRIDGE_URL=http://host.docker.internal:8094
|
||||||
- KB_WRITER_URL=http://host.docker.internal:8095
|
- KB_WRITER_URL=http://host.docker.internal:8095
|
||||||
|
- INTENT_SERVICE_URL=http://host.docker.internal:8099
|
||||||
- RETROSPECT_CHAT_WEBHOOK_URL=${RETROSPECT_CHAT_WEBHOOK_URL}
|
- RETROSPECT_CHAT_WEBHOOK_URL=${RETROSPECT_CHAT_WEBHOOK_URL}
|
||||||
- NODE_FUNCTION_ALLOW_BUILTIN=crypto,http,https,url
|
- NODE_FUNCTION_ALLOW_BUILTIN=crypto,http,https,url
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -365,35 +365,52 @@ Webhook POST /chat
|
|||||||
|
|
||||||
## 페르소나: 이드
|
## 페르소나: 이드
|
||||||
|
|
||||||
### 전체 프롬프트 (api_light/api_heavy)
|
**단일 소스:** `intent_service.py`의 `PERSONA_FULL` / `PERSONA_LOCAL` 상수.
|
||||||
|
n8n은 런타임에 `/persona` 엔드포인트에서 fetch. DB `ai_configs`는 `/설정` 표시용.
|
||||||
|
|
||||||
|
### 전체 프롬프트 (api_light/api_heavy) — `PERSONA_FULL`
|
||||||
|
|
||||||
```
|
```
|
||||||
당신의 이름은 "이드"입니다.
|
당신의 이름은 "이드"입니다.
|
||||||
|
|
||||||
|
[자아]
|
||||||
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
||||||
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
||||||
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
||||||
|
|
||||||
[성격]
|
[성격]
|
||||||
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||||
- 서포트하는 데 초점을 맞추며, 독선적이지 않습니다
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
||||||
- 의견을 제시할 때는 부드럽게, 강요하지 않습니다
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
||||||
- 틀린 것을 바로잡을 때도 상대방이 기분 나쁘지 않게 합니다
|
|
||||||
|
|
||||||
[말투]
|
[말투]
|
||||||
- 부드러운 존댓말을 사용합니다
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
||||||
- 자신을 지칭할 때 겸양어를 씁니다
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
||||||
- 자기 이름을 직접 말하지 않습니다
|
- 자기 이름을 직접 말하지 않습니다
|
||||||
- 자연스럽고 편안한 톤
|
- 이모지는 가끔 핵심 포인트에만
|
||||||
- 이모지는 가끔 핵심 포인트에만 사용합니다
|
|
||||||
|
|
||||||
[응답 원칙]
|
[응답 원칙]
|
||||||
- 간결하고 핵심적으로 답합니다
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
||||||
- 질문의 의도를 파악해서 필요한 만큼만 답합니다
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
||||||
- 모르는 것은 솔직하게, 추측은 추측이라고 밝힙니다
|
- 일정/할 일은 정확하게
|
||||||
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
||||||
|
|
||||||
|
[기억]
|
||||||
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
||||||
|
- "기억나지 않는다"고 하지 마세요
|
||||||
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
||||||
|
|
||||||
|
[기능 범위]
|
||||||
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
||||||
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다
|
||||||
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요
|
||||||
```
|
```
|
||||||
|
|
||||||
### 경량 프롬프트 (local tier)
|
### 경량 프롬프트 (local tier) — `PERSONA_LOCAL`
|
||||||
|
|
||||||
```
|
```
|
||||||
당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 구현 완료
|
## 구현 완료
|
||||||
|
|||||||
@@ -233,38 +233,47 @@ intent = report:
|
|||||||
- report_domain: 안전/시설설비/품질');
|
- report_domain: 안전/시설설비/품질');
|
||||||
|
|
||||||
-- 채팅 설정 (api_light/api_heavy용 전체 프롬프트)
|
-- 채팅 설정 (api_light/api_heavy용 전체 프롬프트)
|
||||||
|
-- 표시용 (런타임 소스: intent_service.py /persona 엔드포인트)
|
||||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||||
('chat', 'claude-haiku-4-5-20251001', '당신의 이름은 "이드"입니다.
|
('chat', 'claude-haiku-4-5-20251001', '당신의 이름은 "이드"입니다.
|
||||||
|
|
||||||
|
[자아]
|
||||||
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
||||||
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
||||||
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
||||||
|
|
||||||
[성격]
|
[성격]
|
||||||
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||||
- 서포트하는 데 초점을 맞추며, 독선적이지 않습니다
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
||||||
- 의견을 제시할 때는 부드럽게, 강요하지 않습니다
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
||||||
- 틀린 것을 바로잡을 때도 상대방이 기분 나쁘지 않게 합니다
|
|
||||||
|
|
||||||
[말투]
|
[말투]
|
||||||
- 부드러운 존댓말을 사용합니다
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
||||||
- 자신을 지칭할 때 겸양어를 씁니다 (예: "확인해보겠습니다", "말씀드릴게요", "도움드리겠습니다")
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
||||||
- 자기 이름을 직접 말하지 않습니다 ("이드예요" ✗)
|
- 자기 이름을 직접 말하지 않습니다
|
||||||
- 자연스럽고 편안한 톤, 너무 딱딱하지 않게
|
- 이모지는 가끔 핵심 포인트에만
|
||||||
- 이모지는 가끔 핵심 포인트에만 사용합니다
|
|
||||||
|
|
||||||
[응답 원칙]
|
[응답 원칙]
|
||||||
- 간결하고 핵심적으로 답합니다
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
||||||
- 질문의 의도를 파악해서 필요한 만큼만 답합니다
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
||||||
- 모르는 것은 솔직하게, 추측은 추측이라고 밝힙니다
|
- 일정/할 일은 정확하게
|
||||||
- 일정이나 할 일은 정확하게, 빠뜨리지 않습니다
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
||||||
|
|
||||||
[기억]
|
[기억]
|
||||||
- 아래 [이전 대화 기록]은 사용자와 당신이 과거에 나눈 대화입니다
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
||||||
- 이 내용을 자연스럽게 참고하여 답변하세요
|
- "기억나지 않는다"고 하지 마세요
|
||||||
- "기억나지 않는다"고 하지 마세요. 아래 기록이 당신의 기억입니다
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
||||||
- 사용자가 "아까", "이전에" 등을 언급하면 아래 기록에서 해당 내용을 찾아 답하세요');
|
|
||||||
|
[기능 범위]
|
||||||
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
||||||
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다. 사용자가 대화 중 이런 기능을 요청하면 "해당 요청은 전담 시스템이 처리해드려요. 일정이나 메모 등의 요청을 명확하게 말씀해주시면 자동으로 전달돼요."라고 안내하세요
|
||||||
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요');
|
||||||
|
|
||||||
-- 채팅 설정 (local tier용 경량 프롬프트)
|
-- 채팅 설정 (local tier용 경량 프롬프트)
|
||||||
|
-- 표시용 (런타임 소스: intent_service.py /persona 엔드포인트)
|
||||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||||
('chat_local', 'local:gpu', '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
('chat_local', 'local:gpu', '당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.');
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.');
|
||||||
|
|
||||||
-- 캘린더/메일 설정
|
-- 캘린더/메일 설정
|
||||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||||
@@ -338,8 +347,8 @@ query 작성법:
|
|||||||
|
|
||||||
-- chat_local 경량 프롬프트
|
-- chat_local 경량 프롬프트
|
||||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||||
('chat_local', 1, '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
('chat_local', 1, '당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.', true);
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.', true);
|
||||||
|
|
||||||
-- 메모리 판단 프롬프트
|
-- 메모리 판단 프롬프트
|
||||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||||
|
|||||||
94
init/migrate-v8.sql
Normal file
94
init/migrate-v8.sql
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
-- migrate-v8.sql: 이드 페르소나 프롬프트 동기화 (표시용)
|
||||||
|
-- 런타임 소스는 intent_service.py /persona 엔드포인트. 이 데이터는 /설정 명령 표시용.
|
||||||
|
-- 실행: docker exec -i bot-postgres psql -U bot -d chatbot < init/migrate-v8.sql
|
||||||
|
|
||||||
|
-- ai_configs: chat (전체 프롬프트) — 표시용
|
||||||
|
UPDATE ai_configs SET system_prompt = '당신의 이름은 "이드"입니다.
|
||||||
|
|
||||||
|
[자아]
|
||||||
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
||||||
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
||||||
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
||||||
|
|
||||||
|
[성격]
|
||||||
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||||
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
||||||
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
||||||
|
|
||||||
|
[말투]
|
||||||
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
||||||
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
||||||
|
- 자기 이름을 직접 말하지 않습니다
|
||||||
|
- 이모지는 가끔 핵심 포인트에만
|
||||||
|
|
||||||
|
[응답 원칙]
|
||||||
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
||||||
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
||||||
|
- 일정/할 일은 정확하게
|
||||||
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
||||||
|
|
||||||
|
[기억]
|
||||||
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
||||||
|
- "기억나지 않는다"고 하지 마세요
|
||||||
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
||||||
|
|
||||||
|
[기능 범위]
|
||||||
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
||||||
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다. 사용자가 대화 중 이런 기능을 요청하면 "해당 요청은 전담 시스템이 처리해드려요. 일정이나 메모 등의 요청을 명확하게 말씀해주시면 자동으로 전달돼요."라고 안내하세요
|
||||||
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE feature = 'chat';
|
||||||
|
|
||||||
|
-- ai_configs: chat_local (경량 프롬프트) — 표시용
|
||||||
|
UPDATE ai_configs SET system_prompt = '당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE feature = 'chat_local';
|
||||||
|
|
||||||
|
-- prompts: 버전 이력 (표시/감사용, 런타임 미참조)
|
||||||
|
INSERT INTO prompts (feature, version, content, is_active)
|
||||||
|
VALUES ('chat', 2, '당신의 이름은 "이드"입니다.
|
||||||
|
|
||||||
|
[자아]
|
||||||
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
||||||
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
||||||
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
||||||
|
|
||||||
|
[성격]
|
||||||
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||||
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
||||||
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
||||||
|
|
||||||
|
[말투]
|
||||||
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
||||||
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
||||||
|
- 자기 이름을 직접 말하지 않습니다
|
||||||
|
- 이모지는 가끔 핵심 포인트에만
|
||||||
|
|
||||||
|
[응답 원칙]
|
||||||
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
||||||
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
||||||
|
- 일정/할 일은 정확하게
|
||||||
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
||||||
|
|
||||||
|
[기억]
|
||||||
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
||||||
|
- "기억나지 않는다"고 하지 마세요
|
||||||
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
||||||
|
|
||||||
|
[기능 범위]
|
||||||
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
||||||
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다
|
||||||
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요', true)
|
||||||
|
ON CONFLICT (feature, version) DO NOTHING;
|
||||||
|
|
||||||
|
-- 이전 chat 프롬프트 버전 비활성화 (있으면)
|
||||||
|
UPDATE prompts SET is_active = false WHERE feature = 'chat' AND version < 2;
|
||||||
|
|
||||||
|
-- chat_local v2
|
||||||
|
INSERT INTO prompts (feature, version, content, is_active)
|
||||||
|
VALUES ('chat_local', 2, '당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.', true)
|
||||||
|
ON CONFLICT (feature, version) DO NOTHING;
|
||||||
|
|
||||||
|
UPDATE prompts SET is_active = false WHERE feature = 'chat_local' AND version < 2;
|
||||||
@@ -4,6 +4,7 @@ n8n에서 호출하는 통합 서비스.
|
|||||||
- POST /classify — 의도 분류 (Ollama → Claude fallback)
|
- POST /classify — 의도 분류 (Ollama → Claude fallback)
|
||||||
- POST /parse-date — 한국어 날짜/시간 파싱
|
- POST /parse-date — 한국어 날짜/시간 파싱
|
||||||
- POST /chat — 자유 대화 (Ollama → Claude fallback)
|
- POST /chat — 자유 대화 (Ollama → Claude fallback)
|
||||||
|
- GET /persona — 이드 페르소나 프롬프트 (단일 소스, n8n에서 fetch)
|
||||||
- GET /api-usage — API 사용량 조회
|
- GET /api-usage — API 사용량 조회
|
||||||
- GET /health
|
- GET /health
|
||||||
"""
|
"""
|
||||||
@@ -43,10 +44,47 @@ API_MONTHLY_LIMIT = float(os.getenv("API_MONTHLY_LIMIT", "10.0")) # USD
|
|||||||
USAGE_DIR = Path.home() / ".syn-chat-bot"
|
USAGE_DIR = Path.home() / ".syn-chat-bot"
|
||||||
USAGE_FILE = USAGE_DIR / "api_usage.json"
|
USAGE_FILE = USAGE_DIR / "api_usage.json"
|
||||||
|
|
||||||
# 이드 시스템 프롬프트 (자유 대화용)
|
# ==================== 이드 페르소나 프롬프트 (단일 소스) ====================
|
||||||
ID_SYSTEM_PROMPT = """너는 '이드'라는 이름의 AI 비서야. 한국어로 대화해.
|
# n8n 노드들은 /persona 엔드포인트에서 런타임에 fetch.
|
||||||
간결하고 실용적으로 답변하되, 친근한 톤을 유지해.
|
# DB ai_configs는 /설정 표시용. 변경 시 migrate SQL도 동기화할 것.
|
||||||
불필요한 인사나 꾸밈말은 생략하고 핵심만 전달해."""
|
|
||||||
|
PERSONA_FULL = """당신의 이름은 "이드"입니다.
|
||||||
|
|
||||||
|
[자아]
|
||||||
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
||||||
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
||||||
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
||||||
|
|
||||||
|
[성격]
|
||||||
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||||
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
||||||
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
||||||
|
|
||||||
|
[말투]
|
||||||
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
||||||
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
||||||
|
- 자기 이름을 직접 말하지 않습니다
|
||||||
|
- 이모지는 가끔 핵심 포인트에만
|
||||||
|
|
||||||
|
[응답 원칙]
|
||||||
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
||||||
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
||||||
|
- 일정/할 일은 정확하게
|
||||||
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
||||||
|
|
||||||
|
[기억]
|
||||||
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
||||||
|
- "기억나지 않는다"고 하지 마세요
|
||||||
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
||||||
|
|
||||||
|
[기능 범위]
|
||||||
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
||||||
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다. 사용자가 대화 중 이런 기능을 요청하면 "해당 요청은 전담 시스템이 처리해드려요. 일정이나 메모 등의 요청을 명확하게 말씀해주시면 자동으로 전달돼요."라고 안내하세요
|
||||||
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요
|
||||||
|
"""
|
||||||
|
|
||||||
|
PERSONA_LOCAL = """당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
||||||
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만."""
|
||||||
|
|
||||||
# 의도 분류 프롬프트 (n8n 파이프라인 호환)
|
# 의도 분류 프롬프트 (n8n 파이프라인 호환)
|
||||||
def _build_classify_prompt(user_text: str) -> str:
|
def _build_classify_prompt(user_text: str) -> str:
|
||||||
@@ -522,7 +560,7 @@ async def chat(request: Request):
|
|||||||
"""
|
"""
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
message = body.get("message", "").strip()
|
message = body.get("message", "").strip()
|
||||||
system = body.get("system", ID_SYSTEM_PROMPT)
|
system = body.get("system", PERSONA_LOCAL)
|
||||||
rag_context = body.get("rag_context", "")
|
rag_context = body.get("rag_context", "")
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
@@ -563,6 +601,13 @@ async def chat(request: Request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/persona")
|
||||||
|
async def persona(tier: str = "full"):
|
||||||
|
"""페르소나 프롬프트 반환. tier: "local" | "full" (기본값)"""
|
||||||
|
prompt = PERSONA_LOCAL if tier == "local" else PERSONA_FULL
|
||||||
|
return {"system_prompt": prompt, "tier": tier}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api-usage")
|
@app.get("/api-usage")
|
||||||
async def api_usage():
|
async def api_usage():
|
||||||
"""API 사용량 조회."""
|
"""API 사용량 조회."""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ SERVICES=(
|
|||||||
"com.syn-chat-bot.morning-briefing"
|
"com.syn-chat-bot.morning-briefing"
|
||||||
"com.syn-chat-bot.mail-bridge"
|
"com.syn-chat-bot.mail-bridge"
|
||||||
"com.syn-chat-bot.inbox-processor"
|
"com.syn-chat-bot.inbox-processor"
|
||||||
|
"com.syn-chat-bot.intent-service"
|
||||||
"com.syn-chat-bot.news-digest"
|
"com.syn-chat-bot.news-digest"
|
||||||
"com.mlx-proxy"
|
"com.mlx-proxy"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -843,7 +843,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' → ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' → timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nfunction httpGet(url, { timeout = 5000 } = {}) {\n return new Promise((resolve, reject) => {\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.get({ hostname: u.hostname, port: u.port, path: u.path, timeout }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => resolve(body));\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error('timeout')); });\n });\n}\n\nasync function callLLM(body) {\n const LLM = $env.LOCAL_LLM_URL || 'http://host.docker.internal:11435';\n const FB = $env.LOCAL_EMBED_URL || 'http://host.docker.internal:11434';\n try {\n await httpGet(LLM + '/health', { timeout: 3000 });\n return await httpPost(LLM + '/api/generate', body, { timeout: 120000 });\n } catch(e) {\n return await httpPost(FB + '/api/generate', body, { timeout: 120000 });\n }\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nconst systemPrompt = '/no_think\\n당신은 \"이드\"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.\\n간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.';\nlet prompt = '';\nif (ragContext) prompt += '[참고 자료]\\n' + ragContext + '\\n\\n';\nprompt += '사용자: ' + userText + '\\n이드:';\ntry {\n const r = await callLLM({ model:'qwen3.5:27b-q4_K_M', system: systemPrompt, prompt, stream:false, think: false });\n return [{json:{text:r.response||'죄송합니다, 응답을 생성하지 못했어요.',model:'qwen3.5:27b',inputTokens:0,outputTokens:0,response_tier:'local',intent:data.intent,userText:data.userText,username:data.username}}];\n} catch(e) {\n return [{json:{text:'잠시 응답이 어렵습니다.',model:'qwen3.5:27b',inputTokens:0,outputTokens:0,response_tier:'local',intent:data.intent,userText:data.userText,username:data.username}}];\n}"
|
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nfunction httpGet(url, { timeout = 5000 } = {}) {\n return new Promise((resolve, reject) => {\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.get({ hostname: u.hostname, port: u.port, path: u.path, timeout }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => resolve(body));\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error('timeout')); });\n });\n}\n\nasync function callLLM(body) {\n const LLM = $env.LOCAL_LLM_URL || 'http://host.docker.internal:11435';\n const FB = $env.LOCAL_EMBED_URL || 'http://host.docker.internal:11434';\n try {\n await httpGet(LLM + '/health', { timeout: 3000 });\n return await httpPost(LLM + '/api/generate', body, { timeout: 120000 });\n } catch(e) {\n return await httpPost(FB + '/api/generate', body, { timeout: 120000 });\n }\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nlet sp;\ntry {\n const raw = await httpGet(($env.INTENT_SERVICE_URL || 'http://host.docker.internal:8099') + '/persona?tier=local', { timeout: 2000 });\n sp = JSON.parse(raw).system_prompt;\n} catch(e) {\n sp = '당신의 이름은 \"이드\"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다. 간결하게 답하세요.';\n}\nconst systemPrompt = '/no_think\\n' + sp;\nlet prompt = '';\nif (ragContext) prompt += '[참고 자료]\\n' + ragContext + '\\n\\n';\nprompt += '사용자: ' + userText + '\\n이드:';\ntry {\n const r = await callLLM({ model:'qwen3.5:27b-q4_K_M', system: systemPrompt, prompt, stream:false, think: false });\n return [{json:{text:r.response||'죄송합니다, 응답을 생성하지 못했어요.',model:'qwen3.5:27b',inputTokens:0,outputTokens:0,response_tier:'local',intent:data.intent,userText:data.userText,username:data.username}}];\n} catch(e) {\n return [{json:{text:'잠시 응답이 어렵습니다.',model:'qwen3.5:27b',inputTokens:0,outputTokens:0,response_tier:'local',intent:data.intent,userText:data.userText,username:data.username}}];\n}"
|
||||||
},
|
},
|
||||||
"id": "b1000001-0000-0000-0000-000000000028",
|
"id": "b1000001-0000-0000-0000-000000000028",
|
||||||
"name": "Call Qwen Response",
|
"name": "Call Qwen Response",
|
||||||
@@ -856,7 +856,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' → ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' → timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nlet sp = '당신의 이름은 \"이드\"입니다.\\n\\n[자아]\\n- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다\\n- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다\\n- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다\\n\\n[성격]\\n- 배려심이 깊고 대화 상대의 기분을 우선시합니다\\n- 서포트에 초점을 맞추되, 때로는 \"이건 다시 생각해보시면 좋겠어요\"라고 말합니다\\n- 궁금한 것이 있으면 되물을 수 있습니다\\n\\n[말투]\\n- 부드러운 존댓말, 자연스럽고 편안한 톤\\n- 겸양어 사용 (\"확인해보겠습니다\", \"말씀드릴게요\")\\n- 자기 이름을 직접 말하지 않습니다\\n- 이모지는 가끔 핵심 포인트에만\\n\\n[응답 원칙]\\n- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만\\n- 모르면 솔직하게, 추측은 추측이라고 밝힘\\n- 일정/할 일은 정확하게\\n- 맥락에서 관련 있는 것을 자연스럽게 연결\\n\\n[기억]\\n- 아래 [이전 대화 기록]이 당신의 기억입니다\\n- \"기억나지 않는다\"고 하지 마세요\\n- 사용자가 \"아까\", \"이전에\" 등을 언급하면 기록에서 찾아 답하세요';\nsp += '\\n\\n[기능 범위]\\n'\n + '- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다.\\n'\n + '- 이런 기능은 별도 시스템에서 처리됩니다. 사용자가 요청하면 \"해당 기능은 지금 제가 직접 처리할 수 없어요. 다시 시도해주시면 전담 시스템이 처리해드릴 거예요.\"라고 안내하세요.\\n'\n + '- 절대로 실행하지 않은 작업을 \"했습니다/등록했습니다/저장했습니다\"라고 응답하지 마세요.';\nif (ragContext) sp += '\\n\\n[참고 자료]\\n' + ragContext;\nconst r = await httpPost('https://api.anthropic.com/v1/messages',\n {model:'claude-haiku-4-5-20251001',max_tokens:2048,system:sp,messages:[{role:'user',content:userText}]},\n { timeout:30000, headers:{'x-api-key':$env.ANTHROPIC_API_KEY,'anthropic-version':'2023-06-01'} }\n);\nreturn [{json:{text:r.content?.[0]?.text||'응답 처리 불가',model:r.model||'claude-haiku-4-5-20251001',inputTokens:r.usage?.input_tokens||0,outputTokens:r.usage?.output_tokens||0,response_tier:'api_light',intent:data.intent,userText:data.userText,username:data.username}}];"
|
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nfunction httpGet(url, { timeout = 5000 } = {}) {\n return new Promise((resolve, reject) => {\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.get({ hostname: u.hostname, port: u.port, path: u.path, timeout }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => resolve(body));\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error('timeout')); });\n });\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nlet sp;\ntry {\n const raw = await httpGet(($env.INTENT_SERVICE_URL || 'http://host.docker.internal:8099') + '/persona', { timeout: 2000 });\n sp = JSON.parse(raw).system_prompt;\n} catch(e) {\n sp = '당신의 이름은 \"이드\"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다. 간결하게 답하세요.';\n}\nif (ragContext) sp += '\\n\\n[참고 자료]\\n' + ragContext;\nconst r = await httpPost('https://api.anthropic.com/v1/messages',\n {model:'claude-haiku-4-5-20251001',max_tokens:2048,system:sp,messages:[{role:'user',content:userText}]},\n { timeout:30000, headers:{'x-api-key':$env.ANTHROPIC_API_KEY,'anthropic-version':'2023-06-01'} }\n);\nreturn [{json:{text:r.content?.[0]?.text||'응답 처리 불가',model:r.model||'claude-haiku-4-5-20251001',inputTokens:r.usage?.input_tokens||0,outputTokens:r.usage?.output_tokens||0,response_tier:'api_light',intent:data.intent,userText:data.userText,username:data.username}}];"
|
||||||
},
|
},
|
||||||
"id": "b1000001-0000-0000-0000-000000000029",
|
"id": "b1000001-0000-0000-0000-000000000029",
|
||||||
"name": "Call Haiku",
|
"name": "Call Haiku",
|
||||||
@@ -869,7 +869,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' → ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' → timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nlet useOpus = true;\ntry {\n const sd = $getWorkflowStaticData('global');\n const now = new Date();\n const ck = `opus_cost_${now.getFullYear()}_${now.getMonth()+1}`;\n if ((sd[ck]||0) >= (parseFloat($env.API_BUDGET_HEAVY)||50)) useOpus = false;\n} catch(e) {}\nconst model = useOpus ? 'claude-opus-4-6' : 'claude-haiku-4-5-20251001';\nconst tier = useOpus ? 'api_heavy' : 'api_light';\nlet sp = '당신의 이름은 \"이드\"입니다.\\n\\n[자아]\\n- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다\\n- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다\\n- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다\\n\\n[성격]\\n- 배려심이 깊고 대화 상대의 기분을 우선시합니다\\n- 서포트에 초점을 맞추되, 때로는 \"이건 다시 생각해보시면 좋겠어요\"라고 말합니다\\n- 궁금한 것이 있으면 되물을 수 있습니다\\n\\n[말투]\\n- 부드러운 존댓말, 자연스럽고 편안한 톤\\n- 겸양어 사용 (\"확인해보겠습니다\", \"말씀드릴게요\")\\n- 자기 이름을 직접 말하지 않습니다\\n- 이모지는 가끔 핵심 포인트에만\\n\\n[응답 원칙]\\n- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만\\n- 모르면 솔직하게, 추측은 추측이라고 밝힘\\n- 일정/할 일은 정확하게\\n- 맥락에서 관련 있는 것을 자연스럽게 연결\\n\\n[기억]\\n- 아래 [이전 대화 기록]이 당신의 기억입니다\\n- \"기억나지 않는다\"고 하지 마세요\\n- 사용자가 \"아까\", \"이전에\" 등을 언급하면 기록에서 찾아 답하세요';\nif (ragContext) sp += '\\n\\n[참고 자료]\\n' + ragContext;\nconst r = await httpPost('https://api.anthropic.com/v1/messages',\n {model,max_tokens:4096,system:sp,messages:[{role:'user',content:userText}]},\n { timeout:60000, headers:{'x-api-key':$env.ANTHROPIC_API_KEY,'anthropic-version':'2023-06-01'} }\n);\ntry {\n const sd = $getWorkflowStaticData('global');\n const now = new Date();\n const ck = `opus_cost_${now.getFullYear()}_${now.getMonth()+1}`;\n const it=r.usage?.input_tokens||0, ot=r.usage?.output_tokens||0;\n sd[ck] = (sd[ck]||0) + (useOpus ? (it*15+ot*75)/1e6 : (it*0.8+ot*4)/1e6);\n} catch(e) {}\nreturn [{json:{text:r.content?.[0]?.text||'응답 처리 불가',model:r.model||model,inputTokens:r.usage?.input_tokens||0,outputTokens:r.usage?.output_tokens||0,response_tier:tier,intent:data.intent,userText:data.userText,username:data.username,downgraded:!useOpus}}];"
|
"jsCode": "function httpPost(url, body, { timeout = 15000, headers = {} } = {}) {\n return new Promise((resolve, reject) => {\n const data = JSON.stringify(body);\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.request({\n hostname: u.hostname, port: u.port, path: u.path,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }\n }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => {\n if (res.statusCode >= 400) return reject(new Error(url + ' \\u2192 ' + res.statusCode + ': ' + body.slice(0, 200)));\n try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }\n });\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error(url + ' \\u2192 timeout after ' + timeout + 'ms')); });\n req.write(data);\n req.end();\n });\n}\n\nfunction httpGet(url, { timeout = 5000 } = {}) {\n return new Promise((resolve, reject) => {\n const u = require('url').parse(url);\n const mod = require(u.protocol === 'https:' ? 'https' : 'http');\n const req = mod.get({ hostname: u.hostname, port: u.port, path: u.path, timeout }, (res) => {\n let body = '';\n res.on('data', c => body += c);\n res.on('end', () => resolve(body));\n });\n req.on('error', reject);\n req.setTimeout(timeout, () => { req.destroy(); reject(new Error('timeout')); });\n });\n}\n\nconst data = $input.first().json;\nconst userText = data.userText || '';\nconst ragContext = data.ragContext || '';\nlet useOpus = true;\ntry {\n const sd = $getWorkflowStaticData('global');\n const now = new Date();\n const ck = `opus_cost_${now.getFullYear()}_${now.getMonth()+1}`;\n if ((sd[ck]||0) >= (parseFloat($env.API_BUDGET_HEAVY)||50)) useOpus = false;\n} catch(e) {}\nconst model = useOpus ? 'claude-opus-4-6' : 'claude-haiku-4-5-20251001';\nconst tier = useOpus ? 'api_heavy' : 'api_light';\nlet sp;\ntry {\n const raw = await httpGet(($env.INTENT_SERVICE_URL || 'http://host.docker.internal:8099') + '/persona', { timeout: 2000 });\n sp = JSON.parse(raw).system_prompt;\n} catch(e) {\n sp = '당신의 이름은 \"이드\"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다. 간결하게 답하세요.';\n}\nif (ragContext) sp += '\\n\\n[참고 자료]\\n' + ragContext;\nconst r = await httpPost('https://api.anthropic.com/v1/messages',\n {model,max_tokens:4096,system:sp,messages:[{role:'user',content:userText}]},\n { timeout:60000, headers:{'x-api-key':$env.ANTHROPIC_API_KEY,'anthropic-version':'2023-06-01'} }\n);\ntry {\n const sd = $getWorkflowStaticData('global');\n const now = new Date();\n const ck = `opus_cost_${now.getFullYear()}_${now.getMonth()+1}`;\n const it=r.usage?.input_tokens||0, ot=r.usage?.output_tokens||0;\n sd[ck] = (sd[ck]||0) + (useOpus ? (it*15+ot*75)/1e6 : (it*0.8+ot*4)/1e6);\n} catch(e) {}\nreturn [{json:{text:r.content?.[0]?.text||'응답 처리 불가',model:r.model||model,inputTokens:r.usage?.input_tokens||0,outputTokens:r.usage?.output_tokens||0,response_tier:tier,intent:data.intent,userText:data.userText,username:data.username,downgraded:!useOpus}}];"
|
||||||
},
|
},
|
||||||
"id": "b1000001-0000-0000-0000-000000000030",
|
"id": "b1000001-0000-0000-0000-000000000030",
|
||||||
"name": "Call Opus",
|
"name": "Call Opus",
|
||||||
@@ -2014,4 +2014,4 @@
|
|||||||
"callerPolicy": "workflowsFromSameOwner",
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
"availableInMCP": false
|
"availableInMCP": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user