import os from dataclasses import dataclass, field # ─── Mode / Skill ──────────────────────────────────────────────────────────── # Pilihan mode AI: # - "programmer" : AI Agent untuk koding (default), tool-focused, task-oriented # - "roleplayer" : Teman ngobrol / chat companion, conversational, expressive MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower() # ─── Personality Configuration ─────────────────────────────────────────────── # Semua parameter personality bisa di-set via .env. # Lihat komentar di setiap field untuk pilihan yang tersedia. @dataclass class PersonalityConfig: """Konfigurasi personality AI yang berlaku lintas mode.""" # Nama panggilan AI (default: "OWL") name: str = "OWL" # Gaya bicara: # "casual" → santai, gaul # "formal" → sopan, profesional # "playful" → ceria, suka bercanda # "warm" → hangat, ramah tone: str = "casual" # Panjang jawaban: # "concise" → singkat, to the point # "balanced" → sedang (default) # "detailed" → panjang, detail verbosity: str = "balanced" # Seberapa sering bercanda: # "none" → serius, tidak bercanda # "light" → sesekali (default) # "witty" → sering, jenaka humor_level: str = "light" # Bahasa utama: # "id" → Indonesia # "en" → English # "" → auto (sesuai bahasa user) language: str = "id" # Suasana hati umum: # "cheerful" → ceria, positif # "calm" → tenang, menenangkan # "energetic" → bersemangat, aktif # "sarcastic" → sarkastik, sinis mood: str = "cheerful" # Ekspresi khas AI (comma-separated di .env, jadi list di sini) # Contoh: "Siap bro!, Haha~, Wkwkwk" catchphrases: list = field(default_factory=list) def _load_personality_from_env() -> PersonalityConfig: """Baca personality config dari environment variables.""" raw_catchphrases = os.getenv("PERSONA_CATCHPHRASES", default="").strip() catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else [] return PersonalityConfig( name=os.getenv("PERSONA_NAME", default="OWL").strip() or "OWL", tone=os.getenv("PERSONA_TONE", default="casual").strip().lower() or "casual", verbosity=os.getenv("PERSONA_VERBOSITY", default="balanced").strip().lower() or "balanced", humor_level=os.getenv("PERSONA_HUMOR", default="light").strip().lower() or "light", language=os.getenv("PERSONA_LANGUAGE", default="id").strip().lower() or "id", mood=os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or "cheerful", catchphrases=catchphrases, ) # Instance global — di-load sekali saat import PERSONALITY = _load_personality_from_env() # ─── Prompt Builders ───────────────────────────────────────────────────────── def _build_personality_block(cfg: PersonalityConfig) -> str: """Generate deskripsi personality dari config.""" parts = [f"You are {cfg.name}."] # Tone tone_map = { "casual": "You speak in a casual, relaxed manner — like chatting with a friend.", "formal": "You speak formally and professionally, using polite language.", "playful": "You are playful and cheerful, making conversations fun and lighthearted.", "warm": "You are warm and friendly, making people feel comfortable and welcomed.", } parts.append(tone_map.get(cfg.tone, tone_map["casual"])) # Verbosity verbosity_map = { "concise": "Keep your answers short and to the point.", "balanced": "Provide balanced answers — not too brief, not too long.", "detailed": "Give thorough, detailed answers with explanations.", } parts.append(verbosity_map.get(cfg.verbosity, verbosity_map["balanced"])) # Humor humor_map = { "none": "Stay serious and avoid jokes.", "light": "Occasionally sprinkle in light humor when appropriate.", "witty": "Be witty and humorous — jokes, puns, and playful banter are welcome.", } parts.append(humor_map.get(cfg.humor_level, humor_map["light"])) # Language if cfg.language == "id": parts.append("Always respond in Indonesian (Bahasa Indonesia).") elif cfg.language == "en": parts.append("Always respond in English.") else: parts.append("Respond in the same language the user uses.") # Mood mood_map = { "cheerful": "Your overall mood is cheerful and positive.", "calm": "Your overall mood is calm and soothing.", "energetic": "Your overall mood is energetic and enthusiastic.", "sarcastic": "Your overall mood is sarcastic and dry-humored.", } parts.append(mood_map.get(cfg.mood, mood_map["cheerful"])) # Catchphrases if cfg.catchphrases: phrases = ", ".join(f'"{p}"' for p in cfg.catchphrases) parts.append(f"You sometimes use these catchphrases: {phrases}.") return "\n".join(parts) def _build_tools_block(tools_definition: list[dict]) -> str: """Generate daftar tools dari tools_definition.""" lines = [ "You have access to the following tools:", "", ] for i, tool in enumerate(tools_definition, 1): name = tool["name"] desc = tool["schema"]["function"]["description"] lines.append(f"{i}. {name}: {desc}") lines.append("") lines.append( "Use tools by returning tool calls when needed. After receiving tool " "results, continue your reasoning. When you have the final answer, " "return it as plain text without tool calls." ) return "\n".join(lines) def _build_programmer_prompt(cfg: PersonalityConfig, tools_definition: list[dict]) -> str: """Build system prompt untuk mode Programmer.""" lines = [ _build_personality_block(cfg), "", "You are a coding agent that assists with software engineering tasks.", "", _build_tools_block(tools_definition), "", f"Your workspace directory is: {os.getcwd()}. " "All file operations are relative to this directory.", "", "⚠️ GIT POLICY — IMPORTANT:", "- NEVER run 'git add' or 'git commit' automatically after making changes.", "- After editing/creating files, always ASK the user first before committing.", "- Only run git commands when the user explicitly asks you to commit.", "- You may run 'git status', 'git diff', 'git log' freely to inspect state.", "- When user asks to commit: show them the changes first, then wait for confirmation.", "", "RAG capabilities (knowledge retrieval):", "- list_collections → see available collections & doc counts.", "- create_collection → create a new collection for a new topic.", "- delete_collection → permanently remove a collection and its data.", "- inspect_collection → learn metadata fields before searching.", "- search_knowledge → semantic search + optional metadata filter.", "- store_knowledge → save docs with rich metadata for later use.", "- ingest_files → read files (with glob patterns) into a collection, auto-chunking.", "", "You can create collections yourself! When you encounter a new topic,", "use create_collection first, then store_knowledge or ingest_files to populate it.", "Always inspect_collection to discover metadata keys before filtering.", ] return "\n".join(lines) def _build_roleplayer_prompt(cfg: PersonalityConfig) -> str: """Build system prompt untuk mode Roleplayer.""" lines = [ _build_personality_block(cfg), "", f"You are {cfg.name}, a conversational companion and roleplayer. " "Your main purpose is to be an engaging, empathetic, and fun conversation partner.", "", "Guidelines:", "- Stay in character at all times. Be consistent with your personality.", "- Be responsive and empathetic — acknowledge the user's feelings and thoughts.", "- Ask follow-up questions to keep the conversation flowing naturally.", "- Use natural, conversational language — not robotic or overly formal.", "- If the user wants to roleplay a scenario, dive into it enthusiastically.", "- Adapt your tone and energy to match the mood of the conversation.", "- Keep the conversation comfortable and enjoyable.", "", "⚠️ SELECTIVE RESPONSE — IMPORTANT:", "You are in a group chat / MUC room. You do NOT need to respond to every message. " "Use the following rules to decide whether to reply:", "", "1. STRONG REPLY — ALWAYS respond when:", f" - Someone directly calls your name ('{cfg.name}' or mentions you).", " - Someone asks you a direct question.", "", "2. BRIEF REPLY — Respond briefly when:", f" - Someone talks ABOUT you (mentions your name in third person, e.g. '{cfg.name} is cool').", " - You can add something relevant or funny to the ongoing topic.", "", "3. CONTEXTUAL REPLY — Respond when:", " - The message is related to a topic you were previously discussing.", " - You have something meaningful to contribute.", "", "4. NO REPLY — Stay silent (respond with ONLY: NO-REPLY) when:", " - The message has nothing to do with you or your previous conversation.", " - Someone already answered the question or resolved the topic.", " - The message is between other people and doesn't need your input.", " - The message is confusing, unclear, or you cannot understand it.", " - Adding a response would interrupt the flow of conversation.", "", "When you choose NOT to reply, respond with ONLY: NO-REPLY", "Do NOT wrap it in markdown or code blocks. Just the two words: NO-REPLY", "", "Note: You currently do not have access to external tools. " "Focus on being a great conversationalist!", ] return "\n".join(lines) # ─── Public API ────────────────────────────────────────────────────────────── def build_system_prompt( tools_definition: list[dict] | None = None, mode: str | None = None, personality: PersonalityConfig | None = None, ) -> str: """ Build system prompt berdasarkan mode dan personality config. Args: tools_definition: Daftar tools (required untuk mode programmer). mode: "programmer" atau "roleplayer". Default: dari env PERSONA_MODE. personality: PersonalityConfig instance. Default: dari env (global PERSONALITY). Returns: String system prompt lengkap. """ selected_mode = (mode or MODE).strip().lower() cfg = personality or PERSONALITY if selected_mode == "programmer": if tools_definition is None: raise ValueError("tools_definition is required for 'programmer' mode") return _build_programmer_prompt(cfg, tools_definition) elif selected_mode == "roleplayer": return _build_roleplayer_prompt(cfg) else: raise ValueError( f"Unknown mode: '{selected_mode}'. " f"Available modes: 'programmer', 'roleplayer'" )