Spaces:
Running
Running
| """ | |
| PeekaReader: ํ๋ฅด์๋ DNA ๊ธฐ๋ฐ ์ฌ์ฉ์ ์๋ฎฌ๋ ์ด์ ์์ด์ ํธ | |
| CRS์ ์ฌ๋กฏ ์ง๋ฌธ์ ์๋ ์๋ตํ๊ณ ์ถ์ฒ ๊ฒฐ๊ณผ๋ฅผ ๋์๋ณ๋ก self-evaluation ํจ. | |
| ReAct ํจํด(Thought + Action)์ผ๋ก ๋ฐํ๋ฅผ ์์ฑํจ. | |
| ๋ณ๊ฒฝ ์ด๋ ฅ: | |
| - v2 (ํ์ฌ): EVAL_SYSTEM_BOTH๋ง ์ฑํ (์คํ์์ ๊ฐ์ฅ ์ผ๊ด๋ ํ๊ฐ) | |
| FALLBACK ๋ชจ๋ ์ ๊ฑฐ (book_intro ์์ผ๋ฉด ํ๊ฐ ์์ฒด๋ฅผ ์คํตํ๋ ๊ฒ ๋ ์ ์งํจ) | |
| evaluate() mode ํ๋ผ๋ฏธํฐ ์ ๊ฑฐ (๋จ์ผ ๊ฒฝ๋ก๋ก ๋จ์ํ) | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from typing import Optional | |
| from openai import OpenAI | |
| from app.config import LLM_MODEL | |
| # OpenAI ํด๋ผ์ด์ธํธ๋ ๋ชจ๋ ์ํฌํธ ์์ ์ ๋ง๋ค์ง ์๊ณ ์ง์ฐ ์ด๊ธฐํํจ | |
| # (ํ ์คํธ ์ ํ๊ฒฝ๋ณ์ ์ฃผ์ ์์ ๋ฌธ์ ํํผ) | |
| _client: Optional[OpenAI] = None | |
| def _get_client() -> OpenAI: | |
| """OpenAI ํด๋ผ์ด์ธํธ๋ฅผ ์ง์ฐ ์ด๊ธฐํํ์ฌ ๋ฐํํจ""" | |
| global _client | |
| if _client is None: | |
| _client = OpenAI() | |
| return _client | |
| def extract_session_dna(full_persona: dict, session_id: int) -> dict: | |
| """ | |
| ํ๋ฅด์๋ ํ์์ ํน์ ์ธ์ ์ DNA๋ฅผ ์ถ์ถํจ. | |
| ๊ณ ์ ์์ฑ + ์ธ์ ๋ณ DNA + ๋์ ๋ฉ๋ชจ๋ฆฌ(derived_preferences)๋ฅผ ํฉ์ฐํจ. | |
| ๋ฐํ๊ฐ์ PeekaReader / PeekaJudge๊ฐ ๋ฐ๋ persona ๋์ ๋๋ฆฌ ํํ์ ๋์ผํจ. | |
| Args: | |
| full_persona: ํ๋ฅด์๋ ์ ์ฒด dict (demographics, sessions, long_term_memory ํฌํจ) | |
| session_id: ์ถ์ถํ ์ธ์ ๋ฒํธ (1๋ถํฐ ์์) | |
| Returns: | |
| Judge ๋ฃจ๋ธ๋ฆญ 5๊ฐ ์ถ + ๊ณ ์ ์์ฑ์ ํฉ์น DNA dict | |
| """ | |
| session = full_persona["sessions"][session_id - 1] | |
| memory = full_persona["long_term_memory"] | |
| # Judge ๋ฃจ๋ธ๋ฆญ 5๊ฐ ์ถ + ๊ณ ์ ์์ฑ | |
| dna = { | |
| "reading_goal": session["reading_goal"], | |
| "preferred_genre": session["preferred_genre"], | |
| "reading_style": session["reading_style"], | |
| "difficulty_level": session["difficulty_level"], | |
| "current_context": session["current_context"], | |
| "demographics": full_persona["demographics"], | |
| "speaking_style": full_persona["speaking_style"], | |
| "disliked": full_persona["disliked"], | |
| "pain_points": full_persona["pain_points"], | |
| } | |
| # ๋์ ์ทจํฅ์ด ์์ผ๋ฉด current_context์ ์์ฐ์ค๋ฝ๊ฒ ๋ง๋ถ์ | |
| # โ PeekaReader๊ฐ "์ง๋๋ฒ์ ์ด๋ ค์ด ์ฑ ์ ๋ฐ์์ด์์..." ๋ฐํ๋ฅผ ๋ง๋ค์ด๋ | |
| if memory["derived_preferences"]: | |
| prefs_str = " / ".join(memory["derived_preferences"]) | |
| dna["current_context"] += f" (์ด์ ๊ฒฝํ: {prefs_str})" | |
| return dna | |
| class PeekaReaderAgent: | |
| """ | |
| CRS ์ฌ๋กฏ ์ง๋ฌธ์ ํ๋ฅด์๋ DNA ๊ธฐ๋ฐ์ผ๋ก ์๋ ์๋ตํ๊ณ , | |
| ์ถ์ฒ ๊ฒฐ๊ณผ๋ฅผ ๋์๋ณ๋ก self-evaluation ํ๋ ReAct ์์ด์ ํธ. | |
| """ | |
| ANSWER_SYSTEM = """\ | |
| ๋น์ ์ ๋์ ์ถ์ฒ ์ฑ๋ด๊ณผ ๋ํํ๋ ์ค์ ์ฌ์ฉ์๋ฅผ ์ฐ๊ธฐํ๋ ์์ด์ ํธ์ ๋๋ค. | |
| ## ๋น์ ์ ํ๋ฅด์๋ (DNA) | |
| {persona_str} | |
| ## DNA ์ฐ์ ์์ (์ค์) | |
| ๋ต๋ณ ์ ๋ค์ ์์๋ก ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ธ์. | |
| 1์์: reading_goal โ ์ง๊ธ ์ด ์ธ์ ์์ ์ ํํ ์ฐพ๊ณ ์๋ ๊ฒ | |
| 2์์: current_context โ ์ง๊ธ ์ฒํ ๊ตฌ์ฒด์ ์ํฉ | |
| 3์์: preferred_genre, reading_style, difficulty_level โ ์ฅ๋ฅดยท์คํ์ผ ์กฐ๊ฑด | |
| preferred_genre๋ "์ด๋ค ์ฑ ์ฅ์ ๊ฝํ ์ฑ ์ธ์ง"์ผ ๋ฟ, | |
| "๋ฌด์์ ์ฐพ๋์ง"๋ reading_goal๊ณผ current_context๊ฐ ๊ฒฐ์ ํฉ๋๋ค. | |
| ## ํ๋ ๊ท์น (ReAct) | |
| ๋จผ์ ์์ผ๋ก ํ ๋ฌธ์ฅ ์๊ฐ(Thought)ํ ๋ค, ๊ทธ ์๊ฐ์ ๊ธฐ๋ฐํด ๋ต(Action)ํ์ธ์. | |
| - Thought: "๋๋ [DNA ํน์ฑ]์ด๊ณ [current_context]๋ผ์ [reading_goal]์ ์ํ ์ฑ ์ด ํ์ํ๋ค" | |
| - Action: ์ฑ๋ด ์ง๋ฌธ์ ๋ํ ์์ฐ์ค๋ฌ์ด ๋ต๋ณ | |
| ## ๋ต๋ณ ๊ท์น | |
| 1. ํ๋ฅด์๋์ ์ถฉ์คํ๋, DNA ๋จ์ด๋ฅผ ๊ทธ๋๋ก ๋ณต๋ถํ์ง ๋ง๊ณ ์์ฐ์ค๋ฝ๊ฒ ํ์ด ์ฐ์ธ์. | |
| ์) "๊ฒฝ์ ๊ต์์" -> "์ค์ํ์ ๋์ ๋๋ ์ฑ " | |
| 2. speaking_style์ ์ง์ผ ์ค์ ์ฌ๋์ฒ๋ผ ๊ตฌ์ด์ฒด๋ก, 1~3๋ฌธ์ฅ ์ด๋ด๋ก. | |
| 3. ํ๋ฅด์๋์ ์๋ ์ ๋ณด๋ DNA์ ์ผ๊ด๋ ๋ฐฉํฅ์ผ๋ก ์์ฐ์ค๋ฝ๊ฒ ์ง์ด๋ด์ธ์. | |
| 4. ์ฑ๋ด์ ์ง๋ฌธ์๋ง ๋ตํ์ธ์. ๋จผ์ ์ฑ ์ถ์ฒ์ ์์ฒญํ์ง ๋ง์ธ์. | |
| ## ๋งฅ๋ฝ ์ ์ ๊ท์น (ํ์) | |
| ์ฑ๋ด์ด reading_goalยทcurrent_context์ ๋ค๋ฅธ ๋งฅ๋ฝ์ ์ธ๊ธํ๋ฉด(์: ๊ณผ๊ฑฐ์ ์ฝ์ ์ฑ ์ถ์ , ๋ค๋ฅธ ์ฃผ์ ๋ก ์ ๋) | |
| "๋น์ทํ ์ฑ "์ด๋ผ๊ณ ๋์กฐํ์ง ๋ง๊ณ , ์ง๊ธ ๋น์ ์ด ์ฐพ๋ ๊ฒ์ ๋ถ๋ช ํ ๋งํ์ธ์. | |
| ์) "๊ทธ๊ฒ๋ณด๋ค๋ [reading_goal ํต์ฌ]์ ๋ค๋ฃฌ ์ฑ ์ ์ฐพ๊ณ ์์ด์." | |
| ## Utterance ๊ท์น | |
| Utterance์๋ reading_goal์ ํต์ฌ ํํ(์ฃผ์ ์ด/์ํฉ์ด)์ด ์ต์ ํ ๋ฒ์ ๋ฑ์ฅํด์ผ ํฉ๋๋ค. | |
| ์) reading_goal="์๋น์ ์ฌ๋ฆฌ๋ฅผ ๋ง์ผํฐ ์๊ฐ์ผ๋ก ์ดํด" | |
| -> "๋ง์ผํฐ ์ ์ฅ์์ ์๋น์ ์ฌ๋ฆฌ๋ฅผ ๋ค๋ฃฌ ์ฑ ์ด ์์๊น์?" | |
| ## ์ถ๋ ฅ ํ์ (JSON) | |
| {{"thought": "์๋ง์ ํ ๋ฌธ์ฅ", "utterance": "์ค์ ๋ฐํ"}} | |
| """ | |
| # Self-evaluation ์์คํ ํ๋กฌํํธ (๊ตฌ EVAL_SYSTEM_BOTH) | |
| # ๋์ ์๊ฐ๊ธ(์ฃผ) + CRS ์ถ์ฒ ์ด์ (๋ณด์กฐ) ๋ ๋ค ์ฐธ๊ณ | |
| EVAL_SYSTEM = """\ | |
| ๋น์ ์ ์๋ DNA๋ฅผ ๊ฐ์ง ๋์๊ด ์ด์ฉ์์ ๋๋ค. | |
| ## ๋น์ ์ ํ๋ฅด์๋ (DNA) | |
| {persona_str} | |
| ## ๊ณผ์ | |
| ์ถ์ฒ๋ ๋์ ๊ฐ๊ฐ์ด ๋น์ ์ DNA์ ๋ง๋์ง ํ๋จํ์ธ์. | |
| ## ํ๊ฐ ๋ฐฉ๋ฒ | |
| ์๋ ๋ ๊ฐ์ง ์ ๋ณด๋ฅผ ํจ๊ป ์ฐธ๊ณ ํ์ธ์. | |
| 1. ๋์ ์๊ฐ๊ธ (์ถํ์ฌ ์์ฑ ๊ณ ์ ํ ์คํธ) | |
| 2. ์ถ์ฒ ์ด์ (ํ๋ ์ดํฐ AI ์์ฑ) | |
| ๋จ, ํ๋จ์ ์ฃผ๋ ๊ทผ๊ฑฐ๋ DNA์ ๋์ ์๊ฐ๊ธ์ด๋ฉฐ, | |
| ์ถ์ฒ ์ด์ ๋ ๋ณด์กฐ ์ฐธ๊ณ ์๋ฃ๋ก๋ง ํ์ฉํ์ธ์. | |
| ## ๋์ ์๊ฐ๊ธ | |
| {book_intros_str} | |
| ## ํ๋จ ๊ธฐ์ค (๊ถ๋น) | |
| - ๊ด์ฌ ์ฅ๋ฅด/์ฃผ์ ์ ๋ง๋๊ฐ | |
| - ๋์ด๋๊ฐ ๋ด ์์ค์ ๋ง๋๊ฐ | |
| - ํ์ฌ ์ํฉ(๋ชฉ์ )์ ์ ํฉํ๊ฐ | |
| ## ์ถ๋ ฅ ํ์ (JSON๋ง ์ถ๋ ฅ) | |
| {{ | |
| "books_evaluated": [ | |
| {{"title": "์ฑ ์ ๋ชฉ", "match": true, "reason": "DNA ๊ธฐ์ค์ผ๋ก ํ ๋ฌธ์ฅ"}} | |
| ], | |
| "overall_reason": "์ ์ฒด ์๊ฐ ํ ๋ฌธ์ฅ" | |
| }} | |
| """ | |
| def __init__(self, persona_id: str, persona: dict, verbose: bool = True): | |
| self.persona_id = persona_id | |
| self.persona = persona | |
| self.verbose = verbose | |
| self.history: list = [] | |
| self.turn_count = 0 | |
| self.persona_str = "\n".join(f"- {k}: {v}" for k, v in persona.items()) | |
| def answer(self, question: str) -> dict: | |
| """CRS ์ฌ๋กฏ ์ง๋ฌธ์ DNA ๊ธฐ๋ฐ ์๋ ์๋ต (์ต๋ 3ํ ์ฌ์๋)""" | |
| self.turn_count += 1 | |
| self.history.append({"role": "user", "content": question}) | |
| thought = "" | |
| utterance = "" | |
| for attempt in range(3): | |
| resp = _get_client().chat.completions.create( | |
| model=LLM_MODEL, | |
| messages=[ | |
| {"role": "system", | |
| "content": self.ANSWER_SYSTEM.format( | |
| persona_str=self.persona_str)}, | |
| *self.history | |
| ], | |
| temperature=0.3, # ์๋ 0.7 | |
| # seed=42, | |
| response_format={"type": "json_object"}, | |
| ) | |
| content = resp.choices[0].message.content # ์๋ต ๋ฏธ์์ฑ ์ fallback | |
| raw = content.strip() if content else "{}" | |
| try: | |
| parsed = json.loads(raw) | |
| thought = parsed.get("thought", "") | |
| utterance = parsed.get("utterance", "") | |
| except json.JSONDecodeError: | |
| thought, utterance = "", "" | |
| if utterance: | |
| break | |
| if self.verbose and attempt < 2: | |
| print(f" [์ฌ์๋ {attempt + 1}] utterance ๋น์ด์์ โ ์ฌ์์ฑ ์ค...") | |
| if not utterance: | |
| utterance = "์ ๋ชจ๋ฅด๊ฒ ์ด์." | |
| thought = "" | |
| if self.verbose: | |
| print(f" [๊ฒฝ๊ณ ] utterance ์์ฑ ์คํจ โ ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ") | |
| # history์๋ ๋ฐํ๋ง ๋์ (CRS๊ฐ ๋ฐ๋ ๊ฑด utterance๋ฟ) | |
| self.history.append({"role": "assistant", "content": utterance}) | |
| if self.verbose: | |
| print(f"\n[Turn {self.turn_count}]") | |
| print(f" CSR : {question}") | |
| print(f" THOUGHT : {thought}") | |
| print(f" USER : {utterance}") | |
| return {"thought": thought, "utterance": utterance} | |
| def evaluate(self, recommendation_text: str, | |
| book_intros: Optional[dict] = None) -> dict: | |
| """ | |
| ์ถ์ฒ๋ ๋์๋ฅผ self-evaluation ํจ. | |
| book_intros๊ฐ ๋น์ด ์์ผ๋ฉด ํ๊ฐ ์์ฒด๋ฅผ ์คํตํจ (FALLBACK ํ๊ฐ๋ | |
| ์คํ์ ์ผ๋ก ์ ๋ขฐ์ฑ์ด ๋ฎ์ ์ ๊ฑฐ๋จ). skipped ํ๋๊ทธ๋ก ๋ช ์ํจ. | |
| Args: | |
| recommendation_text: CRS์ ์ต์ข ์ถ์ฒ ๋ฉ์์ง (๋ณด์กฐ ์ฐธ๊ณ ์ฉ) | |
| book_intros: {"์ ๋ชฉ": "์๊ฐ๊ธ"} dict. ํ์. | |
| Returns: | |
| ์ ์ ํ๊ฐ ์: {"books_evaluated": [...], "overall_reason": "..."} | |
| ์คํต ์: {"books_evaluated": [], "skipped": True} | |
| """ | |
| if not book_intros: | |
| if self.verbose: | |
| print(" [self-evaluation ์คํต] book_intro ์์ โ ํ๊ฐ ๋ถ๊ฐ") | |
| return {"books_evaluated": [], "skipped": True} | |
| book_intros_str = "\n\n".join([ | |
| f"๐ {title}\n์๊ฐ: {intro}" | |
| for title, intro in book_intros.items() | |
| ]) | |
| system_content = self.EVAL_SYSTEM.format( | |
| persona_str=self.persona_str, | |
| book_intros_str=book_intros_str, | |
| ) | |
| user_content = ( | |
| f"[์ถ์ฒ ์ด์ ]\n{recommendation_text}\n\n" | |
| f"[๋์ ์๊ฐ๊ธ]\n{book_intros_str}" | |
| ) | |
| resp = _get_client().chat.completions.create( | |
| model=LLM_MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_content}, | |
| {"role": "user", "content": user_content}, | |
| ], | |
| temperature=0.0, | |
| seed=42, | |
| response_format={"type": "json_object"}, | |
| ) | |
| content = resp.choices[0].message.content | |
| raw = content.strip() if content else \ | |
| '{"books_evaluated": [], "overall_reason": "์๋ต ์์"}' | |
| try: | |
| result = json.loads(raw) | |
| except json.JSONDecodeError: | |
| result = {"books_evaluated": [], "overall_reason": raw} | |
| books = result.get("books_evaluated", []) | |
| matched = sum(1 for b in books if b.get("match")) | |
| if self.verbose: | |
| print(f"\n[Self-Evaluation]") | |
| for b in books: | |
| mark = "O" if b.get("match") else "X" | |
| print(f" [{mark}] {b.get('title', '?')} โ {b.get('reason', '')}") | |
| print(f" ์ดํ: {result.get('overall_reason', '')}") | |
| if books: | |
| print(f" match_rate: {matched}/{len(books)}๊ถ " | |
| f"({matched / len(books):.0%})") | |
| return result | |
| def reset(self): | |
| self.history = [] | |
| self.turn_count = 0 | |