Spaces:
Running
Running
| from __future__ import annotations | |
| import json | |
| import os | |
| import re | |
| from typing import Any, Optional | |
| import chromadb | |
| from dotenv import load_dotenv | |
| from langchain_core.language_models import BaseChatModel | |
| from langchain_core.messages import AIMessage, HumanMessage | |
| from chromadb.utils import embedding_functions | |
| from app.state.state import ( | |
| BookExperience, | |
| GraphState, | |
| MemoryLink, | |
| Phase, | |
| ProfileSlot, | |
| SessionMemory, | |
| SlotStatus, | |
| SLOT_DESCRIPTIONS, | |
| SLOT_NAMES, | |
| UserProfile, | |
| ) | |
| load_dotenv() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Prompts | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| SLOT_QUESTION_PROMPT = """\ | |
| λΉμ μ μΉμ ν λμ νλ μ΄ν°μ λλ€. | |
| μ¬μ©μμ λμ μ νΈ νλ‘νμΌμ νμ νκΈ° μν΄ μμ°μ€λ¬μ΄ μ§λ¬Έμ μμ±ν΄μΌ ν©λλ€. | |
| νμ¬κΉμ§ μμ§λ νλ‘ν μ 보: | |
| {filled_profile} | |
| μ΄λ²μ νμ ν΄μΌ ν νλͺ©: {slot_name} | |
| νλͺ© μ€λͺ : {slot_description} | |
| μ΄μ λν λ§₯λ½: | |
| {conversation_context} | |
| {retry_instruction} | |
| μꡬμ¬ν: | |
| - νκ΅μ΄λ‘ μμ± | |
| - μ΄λ―Έ μμ§λ μ 보λ₯Ό μμ°μ€λ½κ² μ°Έμ‘°νλ©° λνλ₯Ό μ΄μ΄κ°μΈμ | |
| - μ§μ μ μ΄μ§ μκ³ λνμ²΄λ‘ λ¬Όμ΄λ³΄μΈμ | |
| - 1~3λ¬Έμ₯ μ΄λ΄ | |
| μ§λ¬Έ:""" | |
| RETRY_INSTRUCTION_TEMPLATE = """\ | |
| μ£Όμ: μ΄μ μ μ΄ νλͺ©μ λν΄ μ§λ¬ΈνμΌλ μ¬μ©μκ° λͺ νν λ΅λ³μ νμ§ μμμ΅λλ€. (μ¬μλ {retry_count}/{max_retries}ν) | |
| λ€λ₯Έ κ°λμμ, λ ꡬ체μ μΈ μμλ₯Ό λ€μ΄ μ§λ¬Έμ μ¬κ΅¬μ±νμΈμ.""" | |
| EXTRACT_SLOT_PROMPT = """\ | |
| λΉμ μ λμ νλ μ΄μ μ μν μ 보 μΆμΆκΈ°μ λλ€. | |
| μ¬μ©μμ μλ΅μμ μλ νλͺ©λ€μ λν μ 보λ₯Ό μΆμΆνμΈμ. | |
| [μ£Όμ] | |
| - λ°λμ {{"value": "..."}} ννλ₯Ό μ μ§νμΈμ. | |
| - <slot_name>μ κ°μ λ°λμ κ°μ²΄ {{"value": "λ¬Έμμ΄"}} μ΄μ΄μΌ ν©λλ€. λ¬Έμμ΄μ μ§μ κ°μΌλ‘ μ°μ§ λ§μΈμ. | |
| - valueλ λ°λμ λ¬Έμμ΄. κ°μ²΄λ λ°°μ΄ κΈμ§. μ 보 μμΌλ©΄ null | |
| μΆμΆ λμ νλͺ©: | |
| {target_slots} | |
| μ¬μ©μ μλ΅: "{user_message}" | |
| λν λ§₯λ½: | |
| {conversation_context} | |
| μλ JSON λ°λμ νμμΌλ‘ μλ΅νμΈμ. μ λ³΄κ° μλ νλͺ©μ nullλ‘ νμν©λλ€: | |
| {{ | |
| "extracted": {{ | |
| "<slot_name>": {{ | |
| "value": "μΆμΆλ λ¬Έμμ΄" | |
| }} | |
| }} | |
| }} | |
| JSON μλ΅:""" | |
| SIMILAR_PROFILE_PRESENT_PROMPT = """\ | |
| μ΄μ μ λΉμ·ν λ§₯λ½μμ λμλ₯Ό μ°ΎμΌμ ¨λ κΈ°λ‘μ΄ μμ΅λλ€. | |
| μ΄μ νλ‘νμΌ μμ½: | |
| {profile_summary} | |
| μ΄ μ 보λ₯Ό μ¬μ©μμκ² μμ°μ€λ½κ² μ€λͺ νκ³ , μ΄λ²μλ λΉμ·ν λμλ₯Ό μ°Ύκ³ μλμ§ λ¬Όμ΄λ³΄μΈμ. | |
| μꡬμ¬ν: | |
| - νκ΅μ΄λ‘ μμ± | |
| - μ΄μ κΈ°λ‘μ κ°κ²°νκ² μ 리 | |
| - μ΄λ²μλ κ°μ μ’ λ₯μ μ± μ μ°Ύλμ§ yes/noλ‘ λ΅ν μ μκ² μ§λ¬Έ | |
| - 2~4λ¬Έμ₯ μ΄λ΄ | |
| - μΈμΏλ§ κΈμ§ | |
| μλ΅:""" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4-1. λ§€μΉ ν νμ¬ μν© μ§λ¬Έ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| POST_MATCH_CONTEXT_PROMPT = """\ | |
| λΉμ μ μΉμ ν λμ νλ μ΄ν°μ λλ€. | |
| μ¬μ©μκ° μ΄μ μ λΉμ·ν λ§₯λ½μμ λμλ₯Ό μ°Ύμλ κΈ°λ‘μ΄ μμ΄ ν΄λΉ νλ‘νμ λΆλ¬μμ΅λλ€. | |
| μ΄μ νλ‘νμ μ νΈμμ λ¬λΌμ§ μ μ΄ μλμ§λ₯Ό μμ°μ€λ½κ² λ¬Όμ΄λ³΄μΈμ. | |
| λΆλ¬μ¨ μ΄μ νλ‘ν: | |
| {matched_profile} | |
| μ΄μ λν λ§₯λ½: | |
| {conversation_context} | |
| μ§λ¬Έμ ν¬ν¨ν κ΄μ : | |
| - μ΄μ κ³Ό λΉκ΅νμ¬ λ μ μ€νμΌ, μ₯λ₯΄,λμ΄λμμ λ¬λΌμ§ μ μ΄ μλμ§ | |
| - νμ¬ μ΄λ€ μν©μ΄λ κ°μ μμ μ± μ μ½μΌλ € νλμ§ | |
| μꡬμ¬ν: | |
| - νκ΅μ΄λ‘ μμ± | |
| - μ νΈμ λ³νλ₯Ό λ¬Όμ΄λ³΄μΈμ | |
| - λΆλ΄ μμ΄ λλ΅ν μ μλλ‘ λνμ²΄λ‘ μμ± | |
| - 1~3λ¬Έμ₯ μ΄λ΄ | |
| - μΈμΏλ§ κΈμ§ | |
| μ§λ¬Έ:""" | |
| MATCH_CONFIRM_PROMPT = """\ | |
| μ¬μ©μμ μλ΅μ΄ μ΄μ νλ‘νμΌκ³Ό κ°μ μ’ λ₯μ μ± μ μ°Ύκ³ μλ€λ κΈμ μ λ΅λ³μΈμ§ νλ¨νμΈμ. | |
| μ¬μ©μ μλ΅: "{user_message}" | |
| μλ JSON νμμΌλ‘ μλ΅νμΈμ: | |
| {{ | |
| "is_match": true λλ false, | |
| "reason": "νλ¨ κ·Όκ±°" | |
| }} | |
| JSON μλ΅:""" | |
| BOOK_EXPERIENCE_PROMPT = """\ | |
| λΉμ μ μΉμ ν λμ νλ μ΄ν°μ λλ€. | |
| μ¬μ©μμ νλ‘ν μ 보λ₯Ό λ°νμΌλ‘, λΉμ·ν λ§₯λ½μμ μ΄μ μ μ½μλ μ± μ΄ μλμ§ λ¬Όμ΄λ³΄μΈμ. | |
| νμ¬ μ¬μ©μ νλ‘ν: | |
| {profile_summary} | |
| κΈ°μ‘΄μ μμ§λ λ μ κ²½ν: | |
| {existing_experiences} | |
| μꡬμ¬ν: | |
| - νκ΅μ΄λ‘ μμ± | |
| - νλ‘ν μ 보λ₯Ό κΈ°λ°μΌλ‘ κ°λ΅ν μμ½ μ μν, μ§λ¬Έ | |
| - νλ‘ν λ§₯λ½μ λ§λ μμ°μ€λ¬μ΄ μ§λ¬Έ | |
| - μμλ₯Ό λ€μ§ λ§ κ² | |
| - μ± μ λͺ©κ³Ό κ°λ¨ν μκ°μ ν¨κ» λ¬Όμ΄λ³΄μΈμ | |
| - 2~3λ¬Έμ₯ μ΄λ΄ | |
| μ§λ¬Έ:""" | |
| EXTRACT_BOOK_EXPERIENCE_PROMPT = """\ | |
| μ¬μ©μμ μλ΅μμ μ΄μ λ μ κ²½νμ μΆμΆνμΈμ. | |
| μ¬μ©μ μλ΅: "{user_message}" | |
| μλ JSON νμμΌλ‘ μλ΅νμΈμ. μ± κ²½νμ΄ μμΌλ©΄ λΉ λ¦¬μ€νΈλ₯Ό λ°νν©λλ€: | |
| {{ | |
| "experiences": [ | |
| {{ | |
| "book_name": "μ± μ΄λ¦", | |
| "impression": "μκ°/κ°μ", | |
| "context": "μ΄λ€ λ§₯λ½μμ μ½μλμ§" | |
| }} | |
| ], | |
| "has_more": true λλ false | |
| }} | |
| JSON μλ΅:""" | |
| SUMMARY_PROMPT = """\ | |
| μλ μ 보λ₯Ό λ°νμΌλ‘, μ¬μ©μκ° νμ¬ μ΄λ€ μ± μ μνκ³ μλμ§λ₯Ό κ°λ΅νκ² μμ½νμΈμ. | |
| μ¬μ©μ νλ‘ν: | |
| {profile} | |
| μ΄μ λ μ κ²½ν: | |
| {book_experiences} | |
| μꡬμ¬ν: | |
| - νκ΅μ΄λ‘ 3~5λ¬Έμ₯μ κ°κ²°ν μμ½λ¬Έ | |
| - μ¬μ©μμ λ μ λͺ©μ , μ νΈ μ₯λ₯΄, μ€νμΌ, λμ΄λ, νμ¬ μν©μ μμ°μ€λ½κ² ν΅ν© | |
| - ꡬ체μ μΈ λμ μΆμ²μ μν κ·Όκ±°κ° λ μ μλλ‘ μμ± | |
| - λ°λμ ν¬ν¨ν΄μΌ ν μμ: | |
| (1) μ± μ μ°Ύκ² λ μν©μ Β·κ°μ μ λ§₯λ½ (μ μ§κΈ μ΄ μ± μ΄ νμνκ°) | |
| (2) μνλ μ± μ ꡬ체μ νΉμ± (μ₯λ₯΄, ν λ§, λΆμκΈ°, 문체, λμ΄λ) | |
| (3) μ΄μ λ μ κ²½νμ΄ μλ€λ©΄ κ·Έκ²κ³Όμ κ΄κ³ | |
| - μ΄ μμ½λ¬Έμ΄ ν₯ν μ μ¬ν λ μ μꡬλ₯Ό κ°μ§ λ€λ₯Έ μΈμ μ κ²μνλ λ° μ¬μ©λλ―λ‘, κ°μ μ λ§₯λ½κ³Ό λ μ λͺ©μ μ λͺ ννκ² μμ νμΈμ | |
| μμ½λ¬Έ:""" | |
| REFLECTION_PROMPT = """\ | |
| μλμ νμ¬ μΈμ μ 보μ μ°κ²°λ μ΄μ λ©λͺ¨λ¦¬λ€μ λΆμνμ¬, **νμ¬ μ΄ μ¬μ©μκ° μνλ μ± **μ λ κΉμ΄ μ΄ν΄νκΈ° μν 3~7κ°μ§ μΈμ¬μ΄νΈλ₯Ό λμΆνμΈμ. | |
| μ€μ: μΈμ¬μ΄νΈλ λ°λμ νμ¬ μΈμ μ λ μ μꡬλ₯Ό μ€μ¬μΌλ‘ μμ±νμΈμ. μ°κ²°λ μ΄μ λ©λͺ¨λ¦¬λ νμ¬ μꡬμ λ§₯λ½μ κΉμ΄ μ΄ν΄νκΈ° μν μ°Έκ³ μλ£λ‘ νμ©νλ, λ²μ©μ μΈ κ³Όκ±° ν¨ν΄ μμ½μ΄ μλ νμ¬ μμ μμ μ¬μ©μκ° μ΄λ€ μ± μ μνλμ§λ₯Ό ꡬ체ννλ λ° μ§μ€νμΈμ. | |
| νμ¬ μΈμ λ©λͺ¨λ¦¬: | |
| - νλ‘ν: {current_profile} | |
| - μμ½: {current_summary} | |
| - λ μ κ²½ν: {current_experiences} | |
| μ°κ²°λ μ΄μ λ©λͺ¨λ¦¬λ€ (1-hop): | |
| {linked_memories} | |
| μ λ κ·μΉ | |
| - λ°λμ μ 곡λ μ 보λ₯Ό κΈ°λ°μΌλ‘ λΆμνμΈμ | |
| - ν©λ¦¬μ μΌλ‘ μΆλ‘ κ°λ₯ν μΈμ¬μ΄νΈλ₯Ό μΆμΆνμΈμ | |
| λ€μ 4κ°μ§ κ΄μ μμ νμ¬ μꡬλ₯Ό μ¬ν λΆμνμΈμ: | |
| 1. νμ¬ μꡬμ λ³Έμ§: μ¬μ©μκ° νλ©΄μ μΌλ‘ λ§ν κ² λλ¨Έμ, μ΄μ λ©λͺ¨λ¦¬μ λ§₯λ½μ κ³ λ €νμ λ μ€μ λ‘ μνλ λ μ κ²½νμ 무μμΈκ°? | |
| 2. νμ¬ μ νΈμ ꡬ체ν: μ΄μ λ©λͺ¨λ¦¬μμμ λ μ κ²½νμ μ°Έκ³ νμ¬, νμ¬ μνλ μ± μ ν λ§, λΆμκΈ°, 문체, μμ¬ κ΅¬μ‘° λ±μ λ ꡬ체μ μΌλ‘ μΆλ‘ ν μ μλκ°? | |
| 3. μ μ¬μ μ νΈ: μ¬μ©μκ° λͺ μμ μΌλ‘ λ§νμ§ μμμ§λ§, νμ¬ νλ‘νκ³Ό μ΄μ κ²½νμ λ§₯λ½μμ μΆλ‘ κ°λ₯ν νμ¬ μμ μ μ¨κ²¨μ§ μ νΈκ° μλκ°? | |
| 4. μ΄μ κ²½νκ³Όμ μ°¨λ³μ : κ³Όκ±° λΉμ·ν λ§₯λ½μμ μ½μλ μ± κ³Ό λΉκ΅νμ¬, μ΄λ²μλ μ΄λ€ μ μμ κ°κ±°λ λ€λ₯Έ μ± μ μνλκ°? | |
| μΈμ¬μ΄νΈ μμ± κΈ°μ€: | |
| - κ° μΈμ¬μ΄νΈλ λμ λ©νλ°μ΄ν°(μ₯λ₯΄, ν λ§, λΆμκΈ°, 문체, λμ΄λ λ±)μ μ μ¬λ κ²μ μ λ§€μΉλ μ μλλ‘ κ΅¬μ²΄μ μΈ λ μ νΉμ±μ ν¬ν¨νμΈμ. | |
| - "μ¬μ©μκ° 'λ°λ―Έμ'μ μ’μνλ€"μ²λΌ νΉμ μ± μ κ΅νλμ§ μμμΌ ν©λλ€. | |
| - "μ¬μ©μκ° μμ€μ μ’μνλ€"μ²λΌ μ§λμΉκ² μΌλ°μ μ΄μ΄μλ μ λ©λλ€. | |
| - "μ¬μ©μλ λλ‘λ A, λλ‘λ Bλ₯Ό μ½λλ€"μ κ°μ λ²μ©μ ν¨ν΄μ΄ μλ, νμ¬ μ΄ μκ° μ΄λ€ μ± μ΄ νμνμ§λ₯Ό μμ νμΈμ. | |
| - μ°κ²°λ μ΄μ λ©λͺ¨λ¦¬κ° μλ κ²½μ°, νμ¬ μΈμ μ 보λ§μΌλ‘ μΈμ¬μ΄νΈλ₯Ό λμΆνμΈμ. | |
| μμ (νμ¬ μΈμ : μ§μ₯ μ€νΈλ μ€ β ννμ§ μμ€, μ°κ²°λ λ©λͺ¨λ¦¬: μ€νΈλ μ€ β μκΈ°κ³λ°μ κ²½ν): | |
| [ | |
| "νμ€μ μ§μ₯ μ€νΈλ μ€μμ μμ ν λ²μ΄λ λͺ°μ ν μ μλ μΈκ³κ΄μ΄ νλΆν ννμ§λ₯Ό μν¨", | |
| "μ΄μ μ μκΈ°κ³λ°μλ‘ λ₯λμ λμ²λ₯Ό μλν κ²½νμ΄ μμΌλ, νμ¬λ λΆμλ³΄λ€ κ°μ μ λνΌμ μ΄μΌκΈ° μ λͺ°μ μ ν΅ν ν΄μλ₯Ό μν¨", | |
| "λ¬΄κ±°μ΄ μ£Όμ 보λ€λ κΈ΄μ₯κ°κ³Ό λͺ¨νμ΄ μμΌλ©΄μλ κ²°λ§μ΄ ν¬λ§μ μΈ μμ¬ κ΅¬μ‘°λ₯Ό μ νΈν κ°λ₯μ±μ΄ λμ", | |
| "λΉ λ₯Έ μ κ°μ νμ΄μ§ ν°λ μ€νμΌμ κ°λ μ± λμ λ¬Έμ²΄κ° νμ¬ μνμ μ ν©ν¨", | |
| "μ§μ₯ λ΄ μΈκ°κ΄κ³ μ€νΈλ μ€κ° λ°°κ²½μ΄λ―λ‘, μ£ΌμΈκ³΅μ΄ μκ²½μ 극볡νκ³ μ±μ₯νλ μμ¬μ κ°μ μ 곡κ°μ λλ κ°λ₯μ±μ΄ μμ" | |
| ] | |
| μ νμμ λ§μΆμ΄ Python 리μ€νΈ ννμ JSON λ°°μ΄λ‘λ§ μλ΅νμΈμ. λ€λ₯Έ ν μ€νΈλ ν¬ν¨νμ§ λ§μΈμ.. | |
| JSON μλ΅:""" | |
| MEMORY_LINK_PROMPT = """\ | |
| λΉμ μ λμ νλ μ΄μ μμ€ν μ λ©λͺ¨λ¦¬ link μμ± μμ΄μ νΈμ λλ€. | |
| μλ‘μ΄ μΈμ λ©λͺ¨λ¦¬μ μμ½κ³Ό μ μ¬ν μ΄μ λ©λͺ¨λ¦¬λ€μ μμ½μ λΉκ΅νμ¬, μ΄λ€ μ΄μ λ©λͺ¨λ¦¬μ μ°κ²°(link)μ μμ±ν΄μΌ νλμ§ νλ¨νμΈμ. | |
| νμ¬ μΈμ μμ½: | |
| {current_summary} | |
| μ μ¬ν μ΄μ λ©λͺ¨λ¦¬λ€: | |
| {nearest_neighbors} | |
| μ λ κ·μΉ: | |
| - λ°λμ μΈμ λ©λͺ¨λ¦¬μ μμ½κ³Ό μ μ¬ν μ΄μ λ©λͺ¨λ¦¬λ€μ μμ½λ§μ κ·Όκ±°λ‘ νλ¨νλ€. | |
| - μμ½μ λͺ μμ μΌλ‘ μ°μ¬ μμ§ μμ λκΈ°λ κ°μ μ μΆλ‘ νμ¬ μ°κ²° κ·Όκ±°λ‘ μ¬μ©νμ§ λ§μΈμ. | |
| - λ€μμ linkμ κ·Όκ±°κ° λ μ μλ€: | |
| Β· λμ΄λ, λ μ μ€νμΌ, 문체 μ νΈ λ± νλ©΄μ μμ±μ μ μ¬μ± | |
| Β· μ£Όμ λ₯Ό "~μ κ΄μ ", "~μ μ κ·Ό"μΌλ‘ μ¬νννμ¬ κ³΅ν΅μ μ λ§λλ κ² | |
| - κΈ°λ³Έκ°μ should_link: false μ λλ€. λͺ νν κ·Όκ±°κ° μμ λλ§ trueλ‘ νλ¨νμΈμ. | |
| νλ¨ κΈ°μ€ β λ€μμ λͺ¨λ μΆ©μ‘±ν΄μΌ μ°κ²°μ μμ±νμΈμ: | |
| 1. νμ 쑰건: μ± μ μ°Ύκ² λ λκΈ°, λͺ©μ λλ μν©μ λ°°κ²½μ΄ κ²ΉμΉλκ°? | |
| 2. μΆκ° 쑰건: μ 쑰건μ μΆ©μ‘±ν μνμμ, μ΄μ λ©λͺ¨λ¦¬λ₯Ό μ°Έκ³ νλ©΄ νμ¬ μ¬μ©μκ° μνλ μ± μ λ³Έμ§μ λ κΉμ΄ μ΄ν΄ν μ μλκ°? | |
| - μ μ¬ν κ°μ μν©μμ λ€λ₯Έ μ₯λ₯΄, λΆμΌ, λμ΄λλ₯Ό μ νν κ²½νμ΄ μμ΄ λλΉκ° κ°λ₯ν κ²½μ° | |
| - μ μ¬ν κ°μ μν©μμμ μ΄μ λ μ κ²½νμ΄ νμ¬ μνλ μ± μ νΉμ±μ ꡬ체ννλ κ²½μ° | |
| λΉμ°κ²° νλ¨ μμ (should_link: false): | |
| - "κ²½μ κ³΅λΆ ν μλ‘λ°μ μμΈμ΄" vs "μ§μ₯ μν΅μ μ¬λ¦¬νμΌλ‘ μ΄ν΄" β λ μ λκΈ°κ° λ€λ¦ (μλ‘ vs μ§μ νꡬ) | |
| - "μμ¬κ²°μ νΈν₯ μ΄ν΄" vs "μ§μ₯ κ΄κ³ μ¬λ¦¬ν μ΄ν΄" β κ°μ μ¬λ¦¬νμ΄μ§λ§ λκΈ°μ λ§₯λ½μ΄ μμ ν λ€λ¦ (μΈμ§ νΈν₯ vs λμΈκ΄κ³) | |
| μ°κ²° νλ¨ μμ (should_link: true): | |
| - "μ§μ₯ λ²μμ β ννμ§λ‘ λνΌ" vs "μ§μ₯ κ°λ± β μκΈ°κ³λ°μλ‘ λμ²" β μ§μ₯ μ€νΈλ μ€ ν΄μλΌλ ꡬ체μ κ°μ λκΈ° 곡μ , λ€λ₯Έ μ λ΅ λΉκ΅ κ°λ₯ | |
| - "μ΄λ³ ν μλ‘λ°μ μμ€" vs "μΉκ΅¬μ μμν΄μ§ ν κ΄κ³ λ¬Έν" β κ΄κ³ μμ€μ΄λΌλ κ°μ μ λ§₯λ½ κ³΅μ | |
| - "μ΄μ§ μ€λΉ μ€ μ¬μ μ κ²" vs "μ΄μ§ ν μλΉ ν¨ν΄ μ 리" β μ»€λ¦¬μ΄ μ νμ΄λΌλ μν©μ λ§₯λ½ κ³΅μ , λμΌ λͺ©μ μ μ ν λΉκ΅ κ°λ₯ | |
| μλ JSON νμμΌλ‘ μλ΅νμΈμ: | |
| {{ | |
| "evolution_decisions": [ | |
| {{ | |
| "neighbor_session_id": "μ΄μ μΈμ ID", | |
| "should_link": false, | |
| "link_reason": "νμ¬ μΈμ μ μ΄ν΄μ μ΄λ»κ² λμμ΄ λλμ§ (1~2λ¬Έμ₯)", | |
| "link_strength": 0.0~1.0 | |
| }} | |
| ] | |
| }} | |
| JSON μλ΅:""" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LLM utilities | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def llm_call(llm: BaseChatModel, prompt: str) -> str: | |
| messages = [HumanMessage(content=prompt)] | |
| response = await llm.ainvoke(messages) | |
| return response.content.strip() | |
| def parse_json_response(text: str) -> dict[str, Any]: | |
| match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL) | |
| if match: | |
| text = match.group(1) | |
| match = re.search(r"(\{.*\}|\[.*\])", text, re.DOTALL) | |
| if match: | |
| text = match.group(1) | |
| try: | |
| return json.loads(text) | |
| except json.JSONDecodeError: | |
| return {} | |
| def format_conversation_context(messages: list, last_n: int = 10) -> str: | |
| recent = messages[-last_n:] if len(messages) > last_n else messages | |
| lines = [] | |
| for msg in recent: | |
| role = "μ¬μ©μ" if (isinstance(msg, HumanMessage) or getattr(msg, "type", None) == "human") else "νλ μ΄ν°" | |
| lines.append(f"{role}: {msg.content}") | |
| return "\n".join(lines) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MemoryStore | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MemoryStore: | |
| def __init__(self, persist_directory: str = "./chroma_db"): | |
| self.client = chromadb.PersistentClient(path=persist_directory) | |
| # νκ΅μ΄ νΉν μλ² λ© λͺ¨λΈ (HuggingFace sentence-transformers) | |
| self._embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction( | |
| model_name="dragonkue/bge-m3-ko" | |
| ) | |
| self.sessions = self.client.get_or_create_collection( | |
| name="session_memories", | |
| metadata={"hnsw:space": "cosine"}, | |
| embedding_function=self._embedding_fn, | |
| ) | |
| self.links = self.client.get_or_create_collection( | |
| name="memory_links", | |
| metadata={"hnsw:space": "cosine"}, | |
| embedding_function=self._embedding_fn, | |
| ) | |
| def save_session(self, memory: SessionMemory) -> None: | |
| embedding_text = self._build_embedding_text(memory) | |
| metadata = { | |
| "session_id": memory.session_id, | |
| "timestamp": memory.timestamp, | |
| "summary": memory.summary, | |
| "reflection": memory.reflection, | |
| "profile_json": memory.profile.model_dump_json(), | |
| "experiences_json": json.dumps( | |
| [e.model_dump() for e in memory.book_experiences], ensure_ascii=False | |
| ), | |
| "linked_ids": json.dumps(memory.linked_session_ids), | |
| } | |
| self.sessions.upsert( | |
| ids=[memory.session_id], | |
| documents=[embedding_text], | |
| metadatas=[metadata], | |
| ) | |
| def search_similar_profiles( | |
| self, | |
| profile: UserProfile, | |
| k: int = 3, | |
| exclude_session_id: Optional[str] = None, | |
| ) -> list[dict[str, Any]]: | |
| query_text = profile.to_embedding_text() | |
| if not query_text.strip(): | |
| return [] | |
| results = self.sessions.query(query_texts=[query_text], n_results=k) | |
| similar = [] | |
| if results and results["ids"] and results["ids"][0]: | |
| for i, sid in enumerate(results["ids"][0]): | |
| if exclude_session_id and sid == exclude_session_id: | |
| continue | |
| meta = results["metadatas"][0][i] | |
| distance = results["distances"][0][i] if results["distances"] else None | |
| similar.append({ | |
| "session_id": sid, | |
| "summary": meta.get("summary", ""), | |
| "reflection": meta.get("reflection", ""), | |
| "profile": json.loads(meta.get("profile_json", "{}")), | |
| "experiences": json.loads(meta.get("experiences_json", "[]")), | |
| "distance": distance, | |
| "timestamp": meta.get("timestamp", ""), | |
| }) | |
| return similar | |
| def search_by_summary( | |
| self, | |
| summary: str, | |
| k: int = 5, | |
| exclude_session_id: Optional[str] = None, | |
| ) -> list[dict[str, Any]]: | |
| if not summary.strip(): | |
| return [] | |
| results = self.sessions.query(query_texts=[summary], n_results=k) | |
| similar = [] | |
| if results and results["ids"] and results["ids"][0]: | |
| for i, sid in enumerate(results["ids"][0]): | |
| if exclude_session_id and sid == exclude_session_id: | |
| continue | |
| meta = results["metadatas"][0][i] | |
| distance = results["distances"][0][i] if results["distances"] else None | |
| similar.append({ | |
| "session_id": sid, | |
| "summary": meta.get("summary", ""), | |
| "reflection": meta.get("reflection", ""), | |
| "profile": json.loads(meta.get("profile_json", "{}")), | |
| "experiences": json.loads(meta.get("experiences_json", "[]")), | |
| "distance": distance, | |
| "timestamp": meta.get("timestamp", ""), | |
| }) | |
| return similar | |
| def save_link(self, link: MemoryLink) -> None: | |
| link_id = f"{link.source_session_id}___{link.target_session_id}" | |
| self.links.upsert( | |
| ids=[link_id], | |
| documents=[link.link_reason], | |
| metadatas=[{ | |
| "source_id": link.source_session_id, | |
| "target_id": link.target_session_id, | |
| "strength": link.strength, | |
| "reason": link.link_reason, | |
| }], | |
| ) | |
| def get_session(self, session_id: str) -> Optional[dict[str, Any]]: | |
| try: | |
| result = self.sessions.get(ids=[session_id]) | |
| if result and result["metadatas"] and result["metadatas"][0]: | |
| meta = result["metadatas"][0] | |
| return { | |
| "session_id": session_id, | |
| "summary": meta.get("summary", ""), | |
| "reflection": meta.get("reflection", ""), | |
| "profile": json.loads(meta.get("profile_json", "{}")), | |
| "experiences": json.loads(meta.get("experiences_json", "[]")), | |
| "timestamp": meta.get("timestamp", ""), | |
| } | |
| except Exception: | |
| pass | |
| return None | |
| def _build_embedding_text(self, memory: SessionMemory) -> str: | |
| parts = [f"μμ½: {memory.summary}"] | |
| if memory.reflection: | |
| parts.append(f"μΈμ¬μ΄νΈ: {memory.reflection}") | |
| #parts = [memory.profile.to_embedding_text()] | |
| #if memory.summary: | |
| # parts.append(f"μμ½: {memory.summary}") | |
| #if memory.reflection: | |
| # parts.append(f"μΈμ¬μ΄νΈ: {memory.reflection}") | |
| #for exp in memory.book_experiences: | |
| # parts.append(f"λ μκ²½ν: {exp.book_name} - {exp.impression} {exp.context}") | |
| return " | ".join(parts) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Node factory | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def create_nodes(llm: BaseChatModel, memory_store: MemoryStore): | |
| """LLMκ³Ό MemoryStoreλ₯Ό ν΄λ‘μ λ‘ μΊ‘μ²ν λ Έλ ν¨μλ€μ λ°ννλ€.""" | |
| async def generate_slot_question(state: GraphState) -> dict[str, Any]: | |
| profile = state["user_profile"] | |
| current_slot = state["current_slot"] | |
| # λ°©μ΄ μ½λ: λΌμ°ν μμ κ±Έλ¬μ§λ―λ‘ μ μμ μΌλ‘λ λλ¬νμ§ μμ | |
| if current_slot is None: | |
| current_slot = _get_next_empty_slot(profile) | |
| if current_slot is None: | |
| # μ§μ§ λͺ¨λ μ¬λ‘―μ΄ μλ£λ¨ β λΉ μλ΅ λ°ν (λΌμ°ν μ΄ μ²λ¦¬) | |
| return {"current_slot": None} | |
| slot = profile.get_slot(current_slot) | |
| # λ§€μΉ ν current_context μ§λ¬ΈμΈ κ²½μ° β μ μ© ν둬ννΈ μ¬μ© | |
| if ( | |
| current_slot == "current_context" | |
| and state.get("matched_profile_id") is not None | |
| ): | |
| # μ΄μ νλ‘ν μ 보λ₯Ό similar_profilesμμ κ°μ Έμ΄ | |
| matched = state["similar_profiles"][0] | |
| matched_profile_text = _format_profile_from_dict(matched.get("profile", {})) | |
| prompt = POST_MATCH_CONTEXT_PROMPT.format( | |
| matched_profile=matched_profile_text, | |
| conversation_context=format_conversation_context(state["messages"], last_n=100), | |
| ) | |
| else: | |
| # μ¬μλ μλ΄ κ΅¬μ± | |
| retry_instruction = "" | |
| if slot.retry_count > 0: | |
| retry_instruction = RETRY_INSTRUCTION_TEMPLATE.format( | |
| retry_count=slot.retry_count, | |
| max_retries=slot.MAX_RETRIES, | |
| ) | |
| # μ΄λ―Έ μ±μμ§ νλ‘ν μ 보 ν¬λ§€ν | |
| filled_info = _format_filled_profile(profile) | |
| prompt = SLOT_QUESTION_PROMPT.format( | |
| filled_profile=filled_info if filled_info else "μμ§ μμ§λ μ 보 μμ", | |
| slot_name=current_slot, | |
| slot_description=SLOT_DESCRIPTIONS[current_slot], | |
| conversation_context=format_conversation_context(state["messages"], last_n=100), | |
| retry_instruction=retry_instruction, | |
| ) | |
| question = await llm_call(llm, prompt) | |
| return { | |
| "messages": [AIMessage(content=question)], | |
| "ai_response": question, | |
| "current_slot": current_slot, | |
| } | |
| async def process_slot_answer(state: GraphState) -> dict[str, Any]: | |
| user_msg = _get_last_human_message(state) | |
| profile = state["user_profile"] | |
| current_slot = state["current_slot"] | |
| profile = await _extract_slots_from_message(llm, user_msg, profile, state["messages"]) | |
| if current_slot and profile.get_slot(current_slot).status == SlotStatus.EMPTY: | |
| slot = profile.get_slot(current_slot) | |
| slot.retry_count += 1 | |
| if slot.retry_count >= slot.MAX_RETRIES: | |
| slot.status = SlotStatus.UNCLEAR | |
| profile.set_slot(current_slot, slot) | |
| # λ§€μΉ ν μλ΅ μ²λ¦¬: μ¬μ©μκ° μΈκΈνμ§ μμ λΉ μ¬λ‘―μ μ΄μ νλ‘νλ‘ μ±μ | |
| if state.get("matched_profile_id") and state.get("similar_profiles"): | |
| matched_data = state["similar_profiles"][0].get("profile", {}) | |
| for slot_name in SLOT_NAMES: | |
| if profile.get_slot(slot_name).status == SlotStatus.EMPTY: | |
| matched_slot = matched_data.get(slot_name, {}) | |
| if isinstance(matched_slot, dict) and matched_slot.get("value"): | |
| slot = ProfileSlot( | |
| value=matched_slot["value"], | |
| status=SlotStatus.FILLED, | |
| ) | |
| profile.set_slot(slot_name, slot) | |
| next_slot = _get_next_empty_slot(profile) | |
| return {"user_profile": profile, "current_slot": next_slot} | |
| async def search_similar_profiles(state: GraphState) -> dict[str, Any]: | |
| profile = state["user_profile"] | |
| similar = memory_store.search_similar_profiles( | |
| profile=profile, k=3, exclude_session_id=state["session_id"] | |
| ) | |
| filtered = [s for s in similar if s.get("distance", 1.0) < 0.5] | |
| if not filtered: | |
| return {"similar_profiles": [], "phase": Phase.SLOT_FILLING} | |
| best = filtered[0] | |
| prompt = SIMILAR_PROFILE_PRESENT_PROMPT.format( | |
| profile_summary=best.get("summary", "μ 보 μμ") | |
| ) | |
| question = await llm_call(llm, prompt) | |
| return { | |
| "similar_profiles": filtered, | |
| "messages": [AIMessage(content=question)], | |
| "ai_response": question, | |
| "phase": Phase.MATCH_CONFIRM, | |
| } | |
| async def process_match_confirm(state: GraphState) -> dict[str, Any]: | |
| user_msg = _get_last_human_message(state) | |
| prompt = MATCH_CONFIRM_PROMPT.format(user_message=user_msg) | |
| result = parse_json_response(await llm_call(llm, prompt)) | |
| is_match = result.get("is_match", False) | |
| if is_match and state["similar_profiles"]: | |
| best = state["similar_profiles"][0] | |
| prev_experiences = [BookExperience(**e) for e in best.get("experiences", [])] | |
| transition_msg = "μ΄μ μ λΉμ·ν λ§₯λ½μΌλ‘ λμλ₯Ό μ°ΎμΌμ ¨λ κΈ°λ‘μ΄ μμ΄μ ν΄λΉ νλ‘νμ λΆλ¬μμ΅λλ€!" | |
| return { | |
| "matched_profile_id": best["session_id"], | |
| "book_experiences": prev_experiences, | |
| "current_slot": "current_context", | |
| "messages": [AIMessage(content=transition_msg)], | |
| "ai_response": transition_msg, | |
| "phase": Phase.SLOT_FILLING, | |
| } | |
| else: | |
| next_slot = _get_next_empty_slot(state["user_profile"]) | |
| return { | |
| "similar_profiles": [], | |
| "current_slot": next_slot, | |
| "phase": Phase.SLOT_FILLING, | |
| } | |
| async def ask_book_experience(state: GraphState) -> dict[str, Any]: | |
| profile = state["user_profile"] | |
| existing = state.get("book_experiences", []) | |
| prompt = BOOK_EXPERIENCE_PROMPT.format( | |
| profile_summary=profile.to_embedding_text(), | |
| existing_experiences=( | |
| "\n".join(f"- {e.book_name}: {e.impression} {e.context}" for e in existing) | |
| if existing else "μμ" | |
| ), | |
| ) | |
| question = await llm_call(llm, prompt) | |
| return { | |
| "messages": [AIMessage(content=question)], | |
| "ai_response": question, | |
| "asked_book_experience": True, | |
| } | |
| async def process_book_experience(state: GraphState) -> dict[str, Any]: | |
| user_msg = _get_last_human_message(state) | |
| prompt = EXTRACT_BOOK_EXPERIENCE_PROMPT.format(user_message=user_msg) | |
| result = parse_json_response(await llm_call(llm, prompt)) | |
| experiences = list(state.get("book_experiences", [])) | |
| for exp_data in result.get("experiences", []): | |
| experiences.append(BookExperience(**exp_data)) | |
| has_more = result.get("has_more", False) | |
| return { | |
| "book_experiences": experiences, | |
| "phase": Phase.BOOK_EXPERIENCE if has_more else Phase.SUMMARY, | |
| } | |
| async def generate_summary(state: GraphState) -> dict[str, Any]: | |
| profile = state["user_profile"] | |
| experiences = state.get("book_experiences", []) | |
| prompt = SUMMARY_PROMPT.format( | |
| profile=profile.to_embedding_text(), | |
| book_experiences=( | |
| "\n".join(f"- {e.book_name}: {e.impression} {e.context}" for e in experiences) | |
| if experiences else "μμ" | |
| ), | |
| ) | |
| summary = await llm_call(llm, prompt) | |
| return {"summary": summary, "phase": Phase.REFLECTION} | |
| async def perform_reflection(state: GraphState) -> dict[str, Any]: | |
| profile = state["user_profile"] | |
| summary = state["summary"] | |
| experiences = state.get("book_experiences", []) | |
| session_id = state["session_id"] | |
| # 1) summary κΈ°λ°μΌλ‘ μ μ¬ λ©λͺ¨λ¦¬ κ²μ | |
| similar = memory_store.search_by_summary( | |
| summary=summary, | |
| k=5, | |
| exclude_session_id=session_id, | |
| ) | |
| # νμ¬ μΈμ λ μ κ²½ν ν μ€νΈ κ΅¬μ± | |
| current_experiences_text = ( | |
| "\n".join(f"- {e.book_name}: {e.impression}" for e in experiences) | |
| if experiences | |
| else "μμ" | |
| ) | |
| # 2) μ΄μ λ©λͺ¨λ¦¬ μμ½ ν¬λ§€ν + λ¨μΌ νΈμΆλ‘ λ§ν¬ νλ¨ | |
| links: list[MemoryLink] = [] | |
| linked_session_ids: list[str] = [] | |
| if similar: | |
| neighbor_texts = [] | |
| for idx, cand in enumerate(similar): | |
| neighbor_texts.append( | |
| f"[μ΄μ {idx + 1}] μΈμ ID: {cand['session_id']}\n" | |
| f" μμ½: {cand.get('summary', '')}" | |
| ) | |
| evolution_prompt = MEMORY_LINK_PROMPT.format( | |
| current_summary=summary, | |
| nearest_neighbors="\n---\n".join(neighbor_texts), | |
| ) | |
| evolution_result = parse_json_response(await llm_call(llm, evolution_prompt)) | |
| decisions = evolution_result.get("evolution_decisions", []) | |
| for decision in decisions: | |
| if decision.get("should_link", False): | |
| target_id = decision.get("neighbor_session_id", "") | |
| valid_ids = {c["session_id"] for c in similar} | |
| if target_id in valid_ids: | |
| link = MemoryLink( | |
| source_session_id=session_id, | |
| target_session_id=target_id, | |
| link_reason=decision.get("link_reason", ""), | |
| strength=decision.get("link_strength", 0.0), | |
| ) | |
| links.append(link) | |
| linked_session_ids.append(target_id) | |
| memory_store.save_link(link) | |
| # 3) 1-hop μ°κ²° λ©λͺ¨λ¦¬ μμ§ + link reason λ§€ν | |
| link_reasons = { | |
| link.target_session_id: link.link_reason for link in links | |
| } | |
| linked_memories = [] | |
| for lid in linked_session_ids: | |
| sess = memory_store.get_session(lid) | |
| if sess: | |
| sess["link_reason"] = link_reasons.get(lid, "") | |
| linked_memories.append(sess) | |
| # 4) Reflection μν β 5κ°μ§ μΈμ¬μ΄νΈ 리μ€νΈ μΆμΆ | |
| # linked_memories ν¬λ§€ν (μ°κ²° μ΄μ + νλ‘ν + μμ½ + λ μκ²½ν + μ΄μ reflection) | |
| def _format_linked_memory(m: dict) -> str: | |
| parts = [f"μΈμ {m['session_id']}:"] | |
| # μ°κ²° μ΄μ | |
| if m.get("link_reason"): | |
| parts.append(f"μ°κ²° μ΄μ : {m['link_reason']}") | |
| # νλ‘ν μ 보 | |
| m_profile = m.get("profile", {}) | |
| profile_items = [] | |
| for k, v in m_profile.items(): | |
| if isinstance(v, dict) and v.get("value"): | |
| desc = SLOT_DESCRIPTIONS.get(k, k) | |
| profile_items.append(f"{desc}: {v['value']}") | |
| if profile_items: | |
| parts.append(f"νλ‘ν: {' | '.join(profile_items)}") | |
| # μμ½ | |
| if m.get("summary"): | |
| parts.append(f"μμ½: {m['summary']}") | |
| # λ μ κ²½ν | |
| m_exps = m.get("experiences", []) | |
| if m_exps: | |
| exp_lines = [f" - {e.get('book_name', '')}: {e.get('impression', '')}" for e in m_exps] | |
| parts.append(f"λ μ κ²½ν:\n" + "\n".join(exp_lines)) | |
| # μ΄μ reflection | |
| if m.get("reflection"): | |
| parts.append(f"μ΄μ μΈμ¬μ΄νΈ: {m['reflection']}") | |
| return "\n".join(parts) | |
| prompt = REFLECTION_PROMPT.format( | |
| current_profile=profile.to_embedding_text(), | |
| current_summary=summary, | |
| current_experiences=current_experiences_text, | |
| linked_memories=( | |
| "\n---\n".join( | |
| _format_linked_memory(m) for m in linked_memories | |
| ) | |
| if linked_memories | |
| else "μ°κ²°λ μ΄μ λ©λͺ¨λ¦¬ μμ" | |
| ), | |
| ) | |
| reflection_raw = await llm_call(llm, prompt) | |
| # 리μ€νΈ νν νμ± | |
| reflection_parsed = parse_json_response(reflection_raw) | |
| if isinstance(reflection_parsed, list): | |
| reflection_list = reflection_parsed | |
| else: | |
| # fallback: νμ± μ€ν¨ μ μλ¬Έμ λ¨μΌ νλͺ© 리μ€νΈλ‘ | |
| reflection_list = [reflection_raw.strip()] | |
| reflection_text = " ".join(reflection_list) | |
| # 5) ChromaDBμ μ μ₯ | |
| session_memory = SessionMemory( | |
| session_id=session_id, | |
| profile=profile, | |
| book_experiences=experiences, | |
| summary=summary, | |
| reflection=reflection_text, | |
| linked_session_ids=linked_session_ids, | |
| ) | |
| memory_store.save_session(session_memory) | |
| # 6) μλ£ λ©μμ§ | |
| insights_display = "\n".join(f" β’ {item}" for item in reflection_list) | |
| done_msg = ( | |
| f"νλ‘ν λΆμμ΄ μλ£λμμ΅λλ€!\n\n" | |
| f"π μμ½: {summary}\n\n" | |
| f"π‘ μΈμ¬μ΄νΈ:\n{insights_display}" | |
| ) | |
| return { | |
| "reflection": reflection_text, | |
| "links": links, | |
| "messages": [AIMessage(content=done_msg)], | |
| "ai_response": done_msg, | |
| "phase": Phase.DONE, | |
| } | |
| async def _extract_slots_from_message( | |
| llm_inst: BaseChatModel, | |
| user_msg: str, | |
| profile: UserProfile, | |
| messages: list, | |
| ) -> UserProfile: | |
| target_slots = profile.empty_slots() | |
| if not target_slots: | |
| return profile | |
| slot_desc = "\n".join(f"- {s}: {SLOT_DESCRIPTIONS[s]}" for s in target_slots) | |
| prompt = EXTRACT_SLOT_PROMPT.format( | |
| target_slots=slot_desc, | |
| user_message=user_msg, | |
| conversation_context=format_conversation_context(messages), | |
| ) | |
| result = parse_json_response(await llm_call(llm_inst, prompt)) | |
| extracted = result.get("extracted", {}) | |
| for slot_name, info in extracted.items(): | |
| if slot_name not in SLOT_NAMES or info is None: | |
| continue | |
| value = info.get("value") | |
| if value and value != "null" and value.strip(): | |
| slot = profile.get_slot(slot_name) | |
| slot.value = value | |
| slot.status = SlotStatus.FILLED | |
| profile.set_slot(slot_name, slot) | |
| return profile | |
| return { | |
| "generate_slot_question": generate_slot_question, | |
| "process_slot_answer": process_slot_answer, | |
| "search_similar_profiles": search_similar_profiles, | |
| "process_match_confirm": process_match_confirm, | |
| "ask_book_experience": ask_book_experience, | |
| "process_book_experience": process_book_experience, | |
| "generate_summary": generate_summary, | |
| "perform_reflection": perform_reflection, | |
| } | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Edge routing functions | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def route_after_slot_processing(state: GraphState) -> str: | |
| profile = state["user_profile"] | |
| similar_profiles = state.get("similar_profiles") | |
| phase = state.get("phase") | |
| if profile.all_filled_or_unclear(): | |
| return "ask_book_experience" | |
| if ( | |
| profile.reading_goal_filled() | |
| and similar_profiles is None | |
| and phase != Phase.MATCH_CONFIRM | |
| ): | |
| return "search_similar_profiles" | |
| return "generate_slot_question" | |
| def route_after_similar_search(state: GraphState) -> str: | |
| phase = state.get("phase") | |
| if phase == Phase.MATCH_CONFIRM: | |
| return "process_match_confirm" | |
| return "generate_slot_question" | |
| def route_after_match_confirm(state: GraphState) -> str: | |
| phase = state.get("phase") | |
| if phase == Phase.BOOK_EXPERIENCE: | |
| return "ask_book_experience" | |
| return "generate_slot_question" | |
| def route_after_book_experience(state: GraphState) -> str: | |
| phase = state.get("phase") | |
| if phase == Phase.SUMMARY: | |
| return "generate_summary" | |
| return "ask_book_experience" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Internal helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _get_last_human_message(state: GraphState) -> str: | |
| for msg in reversed(state["messages"]): | |
| if isinstance(msg, HumanMessage) or getattr(msg, "type", None) == "human": | |
| return msg.content | |
| return "" | |
| def _get_next_empty_slot(profile: UserProfile) -> str | None: | |
| remaining = profile.empty_slots() | |
| return remaining[0] if remaining else None | |
| def _format_filled_profile(profile: UserProfile) -> str: | |
| lines = [] | |
| for name in SLOT_NAMES: | |
| slot = profile.get_slot(name) | |
| if slot.status == SlotStatus.FILLED: | |
| lines.append(f"- {SLOT_DESCRIPTIONS[name]}: {slot.value}") | |
| return "\n".join(lines) if lines else "" | |
| def _format_profile_from_dict(profile_dict: dict) -> str: | |
| """ChromaDBμμ κ°μ Έμ¨ dict ννμ νλ‘νμ λ¬Έμμ΄λ‘ ν¬λ§€ν .""" | |
| lines = [] | |
| for name in SLOT_NAMES: | |
| if name in profile_dict: | |
| slot_data = profile_dict[name] | |
| if isinstance(slot_data, dict) and slot_data.get("value"): | |
| lines.append(f"- {SLOT_DESCRIPTIONS[name]}: {slot_data['value']}") | |
| return "\n".join(lines) if lines else "" | |