[chore] solution/backend: 에이전트 대화창 다시 염 — AGENT_CHAT_ENABLED 기본 1

카카오톡 채널의 통신사 인증이 끝나 어제 걸어 둔 보류를 푼다. 코드는 어제도 오늘도
그대로고 값만 바꿨다 — 닫고 여는 일이 커밋을 되짚는 일이 되면 안 된다는 어제
판단이 하루 만에 값을 쳤다.

- config/agent_config: 기본값 0 → 1
- ★ 켜도 LLM 키가 없으면 안 열린다(runtime.is_configured 가 스위치와 키를 둘 다
  본다). 키 없는 환경에서 켜 둔 채 잊어도 "눌러도 안 되는 입구" 가 안 생긴다
- 스위치 테스트를 새 기본값에 맞춰 갱신 — 키가 없을 때도 안 열리는 것을 함께 검사

카카오 연결 카드는 아직 감춰져 있다(KAKAO_CHANNEL_PUBLIC_ID 미설정). 채우면 코드는
발급되지만 소비할 웹훅(4단계)이 없어 연결이 완성되지 않는다.

test_agent_runtime·test_kakao_link 34 passed. npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-22 14:17:48 +09:00
parent acd5383b0f
commit 5b54daac10
7 changed files with 59 additions and 36 deletions

View File

@ -83,9 +83,10 @@ ALIMTALK_TEMPLATE_CODE=
# 묶어야 한다. 빌더에서 코드를 받아 채널에 한 번 입력하는 절차다.
# ★ 이 값이 비면 연결 화면이 아예 안 뜬다 — 어디에 코드를 칠지 말해 줄 수 없는데
# 코드만 발급하면 사장님에게는 고장난 화면이다.
# ★ 사장님 대화창(에이전트). 기본 꺼짐 — 카카오톡 채널이 준비되기 전에는 띄우지 않는다.
# 코드는 다 있지만 채널 없이 열어 두면 사장님에게는 어디에도 닿지 않는 입구다.
AGENT_CHAT_ENABLED=0
# 사장님 대화창(에이전트). 1=사용, 0=감춤.
# ★ 1 이어도 LLM 키가 없으면 안 열린다 — 키 없는 환경에서 켜 둔 채 잊어도
# "눌러도 안 되는 입구" 가 생기지 않는다.
AGENT_CHAT_ENABLED=1
KAKAO_CHANNEL_PUBLIC_ID=
KAKAO_LINK_CODE_TTL_MIN=10
KAKAO_LINK_MAX_ATTEMPTS=5

View File

@ -169,9 +169,11 @@
`services/*` 를 통과한다. `collect_service.store_facts` 가 크롤러에 걸어 둔 그 문이다.
- **카카오 채널 발화자는 우리 `user_id` 가 아니다** — 채널 단위 익명 키다.
`owner_kakao_links` 매핑 없이 발화자를 믿으면 **채널 진입점만 소유자 범위 밖**에 놓인다.
- **에이전트 화면은 2026-09-21 기준 감춰져 있다**`AGENT_CHAT_ENABLED=0`(기본) ·
`KAKAO_CHANNEL_PUBLIC_ID` 빔. 카카오톡 채널이 보류돼서이고 **코드는 멀쩡하다**.
"기능이 없다" 고 판단해 지우지 않는다([AGENT.md](docs/AGENT.md)).
- **에이전트 대화창은 스위치와 LLM 키를 둘 다 본다**(`AGENT_CHAT_ENABLED`, 기본 `1`).
키만 보면 "잠시 닫아 두기" 가 키를 지우는 일이 되어 소개문·사진분류까지 꺼지고,
스위치만 보면 키 없는 환경에 **눌러도 안 되는 입구**가 생긴다.
카카오 연결 카드는 `KAKAO_CHANNEL_PUBLIC_ID` 가 비면 감춰진다 —
웹훅(4단계)이 없어 코드를 보내도 연결이 완성되지 않기 때문이다([AGENT.md](docs/AGENT.md)).
- **에이전트 등급을 모델이 정하게 두지 않는다** — 확인이 필요한 행위인지는 `services/agent/tools.py`
레지스트리가 못 박는다. 응답 스키마에 그 칸을 만들면 프롬프트에 끼어든 한 줄이 확인 절차를 건너뛴다.
- **실행 결과 문구를 LLM 이 쓰게 두지 않는다** — 모델은 **하지 않은 일을 했다고 말할 수 있고**,

View File

@ -7,22 +7,24 @@
지금까지 만든 것은 **1단계(신원 연결)****2단계(도구·런타임·빌더 채팅창)** 다.
카카오 채널 웹훅은 아직 없다.
## ★ 지금은 화면에서 감춰져 있다 (2026-09-21 보류)
## 화면 스위치
카카오톡 채널 개설이 **법인폰 본인인증**에 걸려 보류됐다. 채널이 없으면 이 기능은
사장님에게 **어디에도 닿지 않는 입구**다 — 열어 두면 "되는 기능" 으로 오해한다.
| 화면 | 여는 조건 | 지금 |
|---|---|---|
| 대화창(`AgentChatDock`) | `AGENT_CHAT_ENABLED=1`(기본) **그리고** LLM 키 | 열림 |
| 연결 카드(`KakaoChannelCard`) | `KAKAO_CHANNEL_PUBLIC_ID` 가 채워짐 | 채널 ID 미설정 |
| 화면 | 감추는 조건 |
|---|---|
| 대화창(`AgentChatDock`) | `AGENT_CHAT_ENABLED=0` (기본값) |
| 연결 카드(`KakaoChannelCard`) | `KAKAO_CHANNEL_PUBLIC_ID` 가 빔 (기본값) |
★ 스위치와 키를 **둘 다** 본다(`runtime.is_configured`). 키만 보면 "잠시 닫아 두기" 를 키를
지워서 해야 하고 그러면 소개문·사진분류까지 같이 꺼진다. 스위치만 보면 키 없는 환경에서
**눌러도 안 되는 입구**가 생긴다.
**코드는 그대로 두고 설정으로만 닫았다.** 채널이 준비되면 값 둘을 채우고 다시 띄우면 된다 —
되돌릴 때 커밋을 되짚지 않는다. 서버도 함께 닫힌다(`runtime.is_configured()` 가 스위치를
보므로, 화면을 우회해 API 를 직접 불러도 `AGENT_NOT_CONFIGURED` 다).
★ 2026-09-21 에 카카오 채널 개설이 법인폰 본인인증에 걸려 한 번 닫았고,
인증이 끝나 2026-09-22 에 다시 열었다. **그때도 코드는 지우지 않고 값만 바꿨다**
닫고 여는 일이 커밋을 되짚는 일이 되면 안 된다.
★ Threads 카드는 반대로 '자리는 두고 버튼만 죽이는' 쪽이다. 저쪽은 사장님이 **곧 쓸 수 있는**
기능이라 존재를 알려야 했고, 이쪽은 언제 열릴지 말해 줄 수 없다. 판단이 갈린 이유가 그것이다.
★ 연결 카드를 '감추는' 쪽으로 둔 것은 Threads 카드('자리는 두고 버튼만 죽인다')와 반대
판단인데 의도한 차이다. 저쪽은 사장님이 곧 쓸 수 있는 기능이라 존재를 알려야 했고,
이쪽은 웹훅(4단계)이 없어 아직 연결이 **완성되지 않는다**.
## 왜 신원 연결이 먼저인가

View File

@ -1,5 +1,20 @@
# 개발 일지
## 2026-09-22 — 에이전트 대화창 다시 염(기본 켜짐)
카카오톡 채널의 통신사 인증이 끝나 보류를 푼다(사장님 지시). `AGENT_CHAT_ENABLED` 기본값을
`0``1` 로 돌렸다. **코드는 어제도 오늘도 그대로다** — 닫고 여는 일이 커밋을 되짚는 일이
되면 안 된다는 어제 판단이 하루 만에 값을 쳤다.
★ 기본을 켜도 **LLM 키가 없으면 안 열린다**(`runtime.is_configured` 가 스위치와 키를 둘 다
본다). 키 없는 환경에서 켜 둔 채 잊어도 "눌러도 안 되는 입구" 가 생기지 않는다.
★ 카카오 연결 카드는 아직 감춰져 있다 — `KAKAO_CHANNEL_PUBLIC_ID` 미설정.
채우면 코드는 발급되지만 **소비할 웹훅(4단계)이 없어 연결이 완성되지 않는다.**
채널 '채팅' 과 '챗봇(오픈빌더 스킬)' 은 다른 기능이고, 웹훅이 붙는 쪽은 후자다.
**검증** — `test_agent_runtime`(스위치 테스트를 새 기본값에 맞춰 갱신)·`test_kakao_link` 34 passed.
## 2026-09-21 — 에이전트 화면 보류: 설정으로 닫는다(코드는 그대로)
카카오톡 채널 개설이 **법인폰 본인인증**에 걸려 보류됐다(사장님 지시: "이 작업은 여기서 딱

View File

@ -23,10 +23,12 @@ class AgentConfig(BaseSettings):
# 코드가 짧아서(사람이 손으로 친다) 무차별 대입이 가능하다. 시도 수로 끊는다.
KAKAO_LINK_MAX_ATTEMPTS: int = 5
# ★ 기본 꺼짐. 카카오톡 채널 개설이 법인폰 본인인증에 걸려 보류됐고(2026-09-21),
# 채널 없이 대화창만 띄우면 사장님에게는 **어디에도 닿지 않는 입구**가 된다.
# 코드는 그대로 두고 이 값으로만 연다 — 되돌릴 때 커밋을 되짚지 않아도 된다.
AGENT_CHAT_ENABLED: str = "0"
# 빌더 화면의 대화창. 2026-09-21 에 한 번 닫았다가(카카오 채널 보류) 채널 인증이
# 끝나 다시 열었다(2026-09-22).
# ★ 이 값이 "1" 이어도 **LLM 키가 없으면 안 열린다**(runtime.is_configured 가 둘 다 본다) —
# 키 없는 환경에서 켜 둔 채 잊어도 "눌러도 안 되는 입구" 가 생기지 않는다.
# 다시 닫을 일이 생기면 이 값만 "0" 으로 되돌린다. 코드를 되짚지 않는다.
AGENT_CHAT_ENABLED: str = "1"
def get(name, default=""):

View File

@ -45,11 +45,10 @@ class AgentError(RuntimeError):
def is_configured() -> bool:
"""대화창을 열 수 있나 — 스위치와 LLM 키를 **둘 다** 본다.
`AGENT_CHAT_ENABLED` 기본 꺼짐이다(config/agent_config). 카카오톡 채널이 준비되기
전에는 대화창을 띄우지 않는다 코드는 있지만 사장님 입장에서는 어디에도 닿지 않는
입구이고, 열려 있으면 "되는 기능" 으로 오해한다.
이건 Threads 카드처럼 '자리는 두고 버튼만 죽이는' 경우와 다르다. 저쪽은 사장님이
** 있는** 기능이라 존재를 알려야 했고, 이쪽은 아직 제품이 아니다."""
스위치(`AGENT_CHAT_ENABLED`) 키를 ** ** 보는 이유: 키만 보면 "잠시 닫아 두기"
키를 지워서 해야 하는데 그러면 소개문·사진분류까지 같이 꺼진다. 스위치만 보면
없는 환경에서 **눌러도 되는 입구** 생긴다.
실제로 2026-09-21 카카오 채널 보류로 닫았고, 채널 인증이 끝나 다시 열었다."""
return config.chat_enabled() and provider.active().is_configured()

View File

@ -227,20 +227,22 @@ def test_읽기_도구는_확인을_요구하지_않는다():
# ── 보류 스위치 ─────────────────────────────────────────────────────────
def test_스위치가_꺼져_있으면_키가_있어도_안_열린(monkeypatch):
"""기본이 꺼짐이다. 카카오톡 채널이 준비되기 전에는 대화창을 띄우지 않는다 —
코드는 있지만 사장님에게는 어디에도 닿지 않는 입구."""
def test_스위치와_키를_둘_다_본(monkeypatch):
"""키만 보면 '잠시 닫아 두기' 를 키를 지워서 해야 하고, 그러면 소개문·사진분류까지
같이 꺼진다. 스위치만 보면 없는 환경에서 **눌러도 되는 입구** 생긴."""
monkeypatch.setattr(runtime.provider, "active", lambda: SimpleNamespace(is_configured=lambda: True))
monkeypatch.delenv("AGENT_CHAT_ENABLED", raising=False)
assert runtime.is_configured() is False
monkeypatch.setenv("AGENT_CHAT_ENABLED", "1")
assert runtime.is_configured() is True
assert runtime.is_configured() is True # 기본은 켜짐(2026-09-22)
monkeypatch.setenv("AGENT_CHAT_ENABLED", "0")
assert runtime.is_configured() is False
assert runtime.is_configured() is False # 스위치로 닫을 수 있다
monkeypatch.setenv("AGENT_CHAT_ENABLED", "1")
monkeypatch.setattr(runtime.provider, "active", lambda: SimpleNamespace(is_configured=lambda: False))
assert runtime.is_configured() is False # 켜 뒀어도 키가 없으면 안 열린다
async def test_꺼진_동안_대화_요청은_거절된다(client, auth_headers, monkeypatch, db_engine):
monkeypatch.delenv("AGENT_CHAT_ENABLED", raising=False)
async def test_닫아_두면_대화_요청은_거절된다(client, auth_headers, monkeypatch, db_engine):
monkeypatch.setenv("AGENT_CHAT_ENABLED", "0")
h, pid = await seed(client, auth_headers)
res = await client.post(f"/v1/agent/chat/{pid}", headers=h, json={"message": "안녕"})
assert res.status_code == 409