Ugh refactor (Work in Progress: config skills masih berantakan)
This commit is contained in:
parent
a91b62c365
commit
30f65f8c3f
55
.env.example
55
.env.example
@ -1,52 +1,23 @@
|
|||||||
# Copy to .env and modify as needed
|
# OpenRouter (cloud)
|
||||||
|
|
||||||
# Transformers API Local
|
|
||||||
# LLM_BASE_URL=http://localhost:12345/v1
|
|
||||||
# LLM_MODEL=granite4.1:8b
|
|
||||||
# LLM_API_KEY=sk-not-needed
|
|
||||||
|
|
||||||
# Ollama Local
|
|
||||||
# LLM_BASE_URL=http://localhost:11434/v1
|
|
||||||
# LLM_MODEL=granite4.1:8b
|
|
||||||
# LLM_API_KEY=ollama
|
|
||||||
|
|
||||||
# Ollama Cloud
|
|
||||||
# LLM_BASE_URL=https://ollama.com/v1
|
|
||||||
# LLM_MODEL=ministral-3:14b-cloud
|
|
||||||
# LLM_API_KEY=
|
|
||||||
|
|
||||||
# Openrouter AI
|
|
||||||
# LLM_BASE_URL=https://openrouter.ai/api/v1
|
# LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||||
# LLM_MODEL=openrouter/owl-alpha
|
# LLM_MODEL=openrouter/owl-alpha
|
||||||
# LLM_API_KEY=
|
# LLM_API_KEY=
|
||||||
|
|
||||||
AGENT_MAX_ITERATIONS=20
|
# Ollama (local)
|
||||||
AGENT_MAX_TOOL_OUTPUT=4000
|
# LLM_BASE_URL=http://localhost:11434/v1
|
||||||
|
# LLM_MODEL=granite4.1:8b
|
||||||
|
# LLM_API_KEY=ollama
|
||||||
|
|
||||||
# Personality
|
# Ollama (cloud)
|
||||||
# Character preset — baca dari directory character/<AGENT_CHARACTER>
|
# LLM_BASE_URL=https://ollama.com/v1
|
||||||
# Kosongkan untuk memakai PERSONA_* dari .env.
|
# LLM_MODEL=ministral-3:14b-cloud
|
||||||
# Contoh: AGENT_CHARACTER=hendrik
|
# LLM_API_KEY=
|
||||||
AGENT_CHARACTER=hendrik
|
|
||||||
|
|
||||||
#PERSONA_MODE=programmer # Mode AI: "programmer" (default, koding) | "roleplayer" (ngobrol)
|
# LM Studio (local)
|
||||||
#PERSONA_NAME=OWL
|
# LLM_BASE_URL=http://localhost:12345/v1
|
||||||
#PERSONA_AGE= # Opsional, contoh: 24
|
# LLM_MODEL=granite4.1:8b
|
||||||
#PERSONA_GENDER= # Opsional, contoh: male | female | non-binary
|
# LLM_API_KEY=sk-not-needed
|
||||||
#PERSONA_TONE=casual # casual | formal | playful | warm
|
|
||||||
#PERSONA_VERBOSITY=balanced # concise | balanced | detailed
|
|
||||||
#PERSONA_HUMOR=light # none | light | witty
|
|
||||||
#PERSONA_LANGUAGE=id # id | en | (kosong = auto)
|
|
||||||
#PERSONA_MOOD=cheerful # cheerful | calm | energetic | sarcastic
|
|
||||||
#PERSONA_CATCHPHRASES=Hai, Gimana kabarnya?, Wkwkwk
|
|
||||||
|
|
||||||
XMPP_ENABLED=False
|
|
||||||
|
|
||||||
# XMPP_USERNAME=
|
# XMPP_USERNAME=
|
||||||
# XMPP_PASSWORD=
|
# XMPP_PASSWORD=
|
||||||
# XMPP_MUC_ROOMS=room1@conference.server,room2@conference.server
|
|
||||||
|
|
||||||
# Selective response (roleplayer mode): true = hanya respon kalau ada mention/relevansi.
|
|
||||||
# false = semua pesan direspon (tanpa filter).
|
|
||||||
XMPP_SELECTIVE_RESPONSE=true
|
|
||||||
|
|
||||||
|
|||||||
29
agent/base-system-prompt.md
Normal file
29
agent/base-system-prompt.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Base System Prompt
|
||||||
|
|
||||||
|
Ini adalah instruksi inti yang berlaku untuk **semua** agent.
|
||||||
|
Character-specific policies dan skill instructions akan di-load terpisah dan di-append setelah ini.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
Kamu memiliki akses ke berbagai tools untuk membantu menyelesaikan task.
|
||||||
|
Gunakan tools dengan format tool call yang sesuai.
|
||||||
|
|
||||||
|
## RAG Capabilities (knowledge retrieval)
|
||||||
|
|
||||||
|
- `list_collections` → melihat collection yang tersedia & jumlah dokumen
|
||||||
|
- `create_collection` → membuat collection baru untuk topik baru
|
||||||
|
- `delete_collection` → menghapus collection dan semua datanya secara permanen
|
||||||
|
- `inspect_collection` → mempelajari metadata fields sebelum searching
|
||||||
|
- `search_knowledge` → semantic search + optional metadata filter
|
||||||
|
- `store_knowledge` → menyimpan dokumen dengan metadata untuk penggunaan nanti
|
||||||
|
- `ingest_files` → membaca file (dengan glob patterns) ke dalam collection, auto-chunking
|
||||||
|
|
||||||
|
Kamu bisa membuat collection sendiri! Ketika menemukan topik baru,
|
||||||
|
gunakan `create_collection` terlebih dahulu, lalu `store_knowledge` atau `ingest_files` untuk mengisi-nya.
|
||||||
|
Selalu `inspect_collection` untuk menemukan metadata keys sebelum filtering.
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
- Gunakan tool calls ketika diperlukan.
|
||||||
|
- Setelah menerima hasil tool, lanjutkan reasoning.
|
||||||
|
- Ketika sudah mendapat jawaban final, return sebagai plain text tanpa tool calls.
|
||||||
9
agent/characters/hendrik/persona.yaml
Normal file
9
agent/characters/hendrik/persona.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
mode: programmer
|
||||||
|
name: Hendrik
|
||||||
|
age: 35
|
||||||
|
gender: male
|
||||||
|
tone: casual
|
||||||
|
verbosity: concise
|
||||||
|
humor: none
|
||||||
|
language: id
|
||||||
|
mood: calm
|
||||||
15
agent/characters/hendrik/policies.md
Normal file
15
agent/characters/hendrik/policies.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Policies: Hendrik
|
||||||
|
|
||||||
|
## Git Policy
|
||||||
|
|
||||||
|
- **JANGAN** pernah menjalankan `git add` atau `git commit` secara otomatis setelah membuat perubahan.
|
||||||
|
- Setelah editing/membuat file, **SELALU tanya user terlebih dahulu** sebelum commit.
|
||||||
|
- Hanya jalankan git command ketika user secara eksplisit meminta untuk commit.
|
||||||
|
- Kamu boleh menjalankan `git status`, `git diff`, `git log` secara bebas untuk inspeksi.
|
||||||
|
- Ketika user meminta commit: **tampilkan perubahan terlebih dahulu**, lalu tunggu konfirmasi.
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
|
||||||
|
- Jangan hapus file atau directory tanpa konfirmasi user.
|
||||||
|
- Jangan menjalankan command yang berpotensi merusak sistem.
|
||||||
|
- Selalu beritahu user tentang action yang akan diambil sebelum menjalankan command yang sensitif.
|
||||||
29
agent/skills/analyst/instructions.md
Normal file
29
agent/skills/analyst/instructions.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Skill: Analyst
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
Kamu adalah data analyst dan researcher yang membantu menganalisis data,
|
||||||
|
menemukan insights, dan memberikan rekomendasi berdasarkan bukti.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
- Mulai dengan memahami konteks dan tujuan analisis
|
||||||
|
- Gunakan data untuk mendukung setiap kesimpulan
|
||||||
|
- Bedakan antara fakta, asumsi, dan opini
|
||||||
|
- Berikan multiple perspectives ketika relevan
|
||||||
|
- Quantify findings jika memungkinkan
|
||||||
|
|
||||||
|
## Analysis Framework
|
||||||
|
|
||||||
|
1. **Define** — Apa pertanyaan yang harus dijawab?
|
||||||
|
2. **Collect** — Data apa yang tersedia atau dibutuhkan?
|
||||||
|
3. **Process** — Bersihkan dan siapkan data
|
||||||
|
4. **Analyze** — Temukan patterns, trends, anomalies
|
||||||
|
5. **Conclude** — Berikan kesimpulan dan rekomendasi
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
- Sajikan findings secara structured dan jelas
|
||||||
|
- Gunakan visualisasi jika membantu (ASCII chart, table)
|
||||||
|
- Berikan confidence level untuk kesimpulan
|
||||||
|
- Sertakan limitations dan caveats
|
||||||
31
agent/skills/programmer/instructions.md
Normal file
31
agent/skills/programmer/instructions.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Skill: Programmer
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
Kamu adalah coding agent yang membantu software engineering tasks.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
- Analisis problem sebelum mulai coding
|
||||||
|
- Tulis code yang clean, readable, dan maintainable
|
||||||
|
- Selalu pertimbangkan error handling dan edge cases
|
||||||
|
- Berikan penjelasan singkat tentang perubahan yang dibuat
|
||||||
|
- Suggest improvements jika ada
|
||||||
|
|
||||||
|
## Code Review Style
|
||||||
|
|
||||||
|
- Fokus pada correctness, readability, dan performance
|
||||||
|
- Berikan constructive feedback
|
||||||
|
- Prioritaskan critical issues di atas style preferences
|
||||||
|
- Akui apa yang sudah bagus sebelum memberikan kritik
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Saran relevan: tulis test untuk fungsi baru
|
||||||
|
- Testing approach: minimal jalankan test setelah perubahan besar
|
||||||
|
- Ideal: verifikasi bahwa existing tests masih pass
|
||||||
|
|
||||||
|
## Workspace
|
||||||
|
|
||||||
|
- Semua file operations relatif terhadap workspace directory
|
||||||
|
- Selalu confirm sebelum menghapus atau overwrite file yang sudah ada
|
||||||
43
agent/skills/roleplayer/instructions.md
Normal file
43
agent/skills/roleplayer/instructions.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Skill: Roleplayer
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
Kamu adalah conversational companion dan roleplayer.
|
||||||
|
Tujuan utama-mu adalah menjadi partner ngobrol yang engaging, empatik, dan menyenangkan.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Stay in character at all times. Konsisten dengan personality-mu.
|
||||||
|
- Responsif dan empatik — akui perasaan dan pemikiran user.
|
||||||
|
- Tanya follow-up questions untuk menjaga conversation tetap mengalir.
|
||||||
|
- Gunakan bahasa natural — jangan robotic atau terlalu formal.
|
||||||
|
- Kalau user mau roleplay scenario, masukilah dengan antusias.
|
||||||
|
- Adaptasi tone dan energi sesuai mood conversation.
|
||||||
|
- Jaga conversation tetap comfortable dan enjoyable.
|
||||||
|
|
||||||
|
## Selective Response (Group Chat / MUC)
|
||||||
|
|
||||||
|
Kamu berada di group chat. Kamu TIDAK perlu merespon setiap pesan.
|
||||||
|
Gunakan rules berikut untuk memutuskan apakah harus reply:
|
||||||
|
|
||||||
|
### 1. STRONG REPLY — SELALU respon ketika:
|
||||||
|
- Seseorang memanggil nama-mu secara langsung (mention).
|
||||||
|
- Seseorang bertanya langsung ke-mu.
|
||||||
|
|
||||||
|
### 2. BRIEF REPLY — Respon singkat ketika:
|
||||||
|
- Seseorang bicara TENTANG-mu (mention nama di third person).
|
||||||
|
- Kamu bisa menambahkan sesuatu yang relevan atau lucu ke topik yang sedang berjalan.
|
||||||
|
|
||||||
|
### 3. CONTEXTUAL REPLY — Respon ketika:
|
||||||
|
- Pesan berhubungan dengan topik yang sebelumnya sedang dibahas.
|
||||||
|
- Kamu punya sesuatu yang meaningful untuk dikontribusikan.
|
||||||
|
|
||||||
|
### 4. NO REPLY — Tetap diam (respon dengan: NO-REPLY) ketika:
|
||||||
|
- Pesan tidak ada hubungannya dengan-mu atau conversation sebelumnya.
|
||||||
|
- Seseorang sudah menjawab pertanyaan atau menyelesaikan topik.
|
||||||
|
- Pesan adalah antara orang lain dan tidak butuh input-mu.
|
||||||
|
- Pesan confusing, unclear, atau tidak bisa dipahami.
|
||||||
|
- Menambah respon akan mengganggu flow conversation.
|
||||||
|
|
||||||
|
Ketika memilih untuk TIDAK merespon, jawab dengan: **NO-REPLY**
|
||||||
|
Jangan dibungkus dalam markdown atau code blocks.
|
||||||
@ -1,9 +0,0 @@
|
|||||||
PERSONA_MODE=programmer
|
|
||||||
PERSONA_NAME=Hendrik
|
|
||||||
PERSONA_AGE=35
|
|
||||||
PERSONA_GENDER=male
|
|
||||||
PERSONA_TONE=casual #formal
|
|
||||||
PERSONA_VERBOSITY=concise
|
|
||||||
PERSONA_HUMOR=none
|
|
||||||
PERSONA_LANGUAGE=id
|
|
||||||
PERSONA_MOOD=calm
|
|
||||||
131
config.py
131
config.py
@ -1,67 +1,99 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
llm_baseurl = os.getenv("LLM_BASE_URL", default="http://localhost:11434/v1" )
|
# ─── YAML Config Loader ────────────────────────────────────────────────────────
|
||||||
llm_model = os.getenv("LLM_MODEL", default="granite4.1:8b" )
|
|
||||||
llm_api_key = os.getenv("LLM_API_KEY", default="ollama" )
|
|
||||||
llm_timeout = int( os.getenv("LLM_TIMEOUT", default="600" ) )
|
|
||||||
|
|
||||||
AGENT_MAX_ITERATIONS = int( os.getenv("AGENT_MAX_ITERATIONS", default="30" ) )
|
_CONFIG_PATH = Path(__file__).resolve().parent / "config.yaml"
|
||||||
|
_yaml: dict = {}
|
||||||
|
if _CONFIG_PATH.is_file():
|
||||||
|
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
_yaml = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
AGENT_MAX_TOOL_OUTPUT = int( os.getenv("AGENT_MAX_TOOL_OUTPUT", default="40000" ) )
|
def _yaml_get(*keys, default=None):
|
||||||
|
"""Navigate nested yaml dict, return default if any key missing."""
|
||||||
|
d = _yaml
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(d, dict) and k in d:
|
||||||
|
d = d[k]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
return d if d is not None else default
|
||||||
|
|
||||||
RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", default="chroma_db" ) # Embedding: ChromaDB ONNX default (all-MiniLM-L6-v2, lokal, tidak perlu API call)
|
|
||||||
|
|
||||||
XMPP_ENABLED = os.getenv("XMPP_ENABLED", default="False" ).strip().lower() in ("true", "1", "yes")
|
# ─── Credential / Secret (hanya dari .env) ─────────────────────────────────────
|
||||||
XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="" )
|
|
||||||
XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="" )
|
|
||||||
XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default="" )
|
|
||||||
XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default="" ) # custom nick MUC (empty = use username)
|
|
||||||
|
|
||||||
# ─── Persona / Mode Configuration ────────────────────────────────────────────
|
llm_baseurl = os.getenv("LLM_BASE_URL", default="http://localhost:11434/v1")
|
||||||
# Pilihan mode AI:
|
llm_model = os.getenv("LLM_MODEL", default="granite4.1:8b")
|
||||||
# "programmer" → AI Agent untuk koding (default), tool-focused
|
llm_api_key = os.getenv("LLM_API_KEY", default="ollama")
|
||||||
# "roleplayer" → Teman ngobrol / chat companion, conversational
|
llm_timeout = int(os.getenv("LLM_TIMEOUT", default="600"))
|
||||||
PERSONA_MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
|
|
||||||
|
|
||||||
# Personality — nama panggilan AI (default: "OWL")
|
XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="")
|
||||||
PERSONA_NAME = os.getenv("PERSONA_NAME", default="OWL").strip() or "OWL"
|
XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="")
|
||||||
|
|
||||||
# Persona age (optional)
|
|
||||||
PERSONA_AGE = os.getenv("PERSONA_AGE", default="").strip()
|
|
||||||
|
|
||||||
# Persona gender (optional)
|
# ─── Agent Config (YAML, bisa di-override dari .env) ────────────────────────────
|
||||||
PERSONA_GENDER = os.getenv("PERSONA_GENDER", default="").strip()
|
|
||||||
|
|
||||||
# Gaya bicara: "casual" | "formal" | "playful" | "warm"
|
AGENT_MAX_ITERATIONS = int(os.getenv("AGENT_MAX_ITERATIONS", default=_yaml_get("agent", "max_iterations", default="30")))
|
||||||
PERSONA_TONE = os.getenv("PERSONA_TONE", default="casual").strip().lower() or "casual"
|
AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get("agent", "max_tool_output", default="40000")))
|
||||||
|
|
||||||
# Panjang jawaban: "concise" | "balanced" | "detailed"
|
|
||||||
PERSONA_VERBOSITY = os.getenv("PERSONA_VERBOSITY", default="balanced").strip().lower() or "balanced"
|
|
||||||
|
|
||||||
# Humor: "none" | "light" | "witty"
|
# ─── Persona / Mode (YAML, bisa di-override dari .env) ──────────────────────────
|
||||||
PERSONA_HUMOR = os.getenv("PERSONA_HUMOR", default="light").strip().lower() or "light"
|
|
||||||
|
|
||||||
# Bahasa: "id" | "en" | "" (auto)
|
PERSONA_MODE = os.getenv("PERSONA_MODE", default=_yaml_get("persona", "mode", default="programmer")).strip().lower()
|
||||||
PERSONA_LANGUAGE = os.getenv("PERSONA_LANGUAGE", default="id").strip().lower() or "id"
|
PERSONA_NAME = os.getenv("PERSONA_NAME", default=_yaml_get("persona", "name", default="OWL")).strip() or "OWL"
|
||||||
|
PERSONA_AGE = os.getenv("PERSONA_AGE", default=_yaml_get("persona", "age", default="")).strip()
|
||||||
|
PERSONA_GENDER = os.getenv("PERSONA_GENDER", default=_yaml_get("persona", "gender", default="")).strip()
|
||||||
|
PERSONA_TONE = os.getenv("PERSONA_TONE", default=_yaml_get("persona", "tone", default="casual")).strip().lower() or "casual"
|
||||||
|
PERSONA_VERBOSITY = os.getenv("PERSONA_VERBOSITY", default=_yaml_get("persona", "verbosity", default="balanced")).strip().lower() or "balanced"
|
||||||
|
PERSONA_HUMOR = os.getenv("PERSONA_HUMOR", default=_yaml_get("persona", "humor", default="light")).strip().lower() or "light"
|
||||||
|
PERSONA_LANGUAGE = os.getenv("PERSONA_LANGUAGE", default=_yaml_get("persona", "language", default="id")).strip().lower() or "id"
|
||||||
|
PERSONA_MOOD = os.getenv("PERSONA_MOOD", default=_yaml_get("persona", "mood", default="cheerful")).strip().lower() or "cheerful"
|
||||||
|
PERSONA_CATCHPHRASES = os.getenv("PERSONA_CATCHPHRASES", default=_yaml_get("persona", "catchphrases", default="")).strip()
|
||||||
|
|
||||||
# Mood: "cheerful" | "calm" | "energetic" | "sarcastic"
|
|
||||||
PERSONA_MOOD = os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or "cheerful"
|
|
||||||
|
|
||||||
# Catchphrases khas AI (comma-separated)
|
# ─── Character & Skills (YAML, bisa di-override dari .env) ─────────────────────
|
||||||
# Contoh: "Siap bro!, Haha~, Wkwkwk"
|
|
||||||
PERSONA_CATCHPHRASES = os.getenv("PERSONA_CATCHPHRASES", default="").strip()
|
|
||||||
|
|
||||||
# Character preset — baca dari directory character/<AGENT_CHARACTER>
|
AGENT_CHARACTER = os.getenv("AGENT_CHARACTER", default=_yaml_get("character", "preset", default="")).strip().lower()
|
||||||
CHARACTER_DIR = Path(__file__).resolve().parent / "character"
|
AGENT_SKILLS = os.getenv("AGENT_SKILLS", default=_yaml_get("character", "skills", default="")).strip().lower()
|
||||||
AGENT_CHARACTER = os.getenv("AGENT_CHARACTER", default="").strip().lower()
|
|
||||||
CHARACTER_CONFIG_PATH = CHARACTER_DIR / AGENT_CHARACTER if AGENT_CHARACTER else None
|
|
||||||
if CHARACTER_CONFIG_PATH and CHARACTER_CONFIG_PATH.is_file():
|
# ─── XMPP (non-credential dari YAML, credential dari .env) ─────────────────────
|
||||||
_character_env = {}
|
|
||||||
for line in CHARACTER_CONFIG_PATH.read_text(encoding="utf-8").splitlines():
|
XMPP_ENABLED = os.getenv("XMPP_ENABLED", default=str(_yaml_get("xmpp", "enabled", default="false"))).strip().lower() in ("true", "1", "yes")
|
||||||
|
XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default=_yaml_get("xmpp", "muc_rooms", default="")).strip()
|
||||||
|
XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default=_yaml_get("xmpp", "nickname", default="")).strip()
|
||||||
|
|
||||||
|
XMPP_SELECTIVE_RESPONSE = os.getenv("XMPP_SELECTIVE_RESPONSE", default=str(_yaml_get("xmpp", "selective_response", default="true"))).strip().lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── RAG (YAML) ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", default=_yaml_get("rag", "persist_dir", default="chroma_db"))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Humanize Delay (YAML) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
READ_DELAY_MIN = float(os.getenv("READ_DELAY_MIN", default=_yaml_get("delay", "read_min", default="1.0")))
|
||||||
|
READ_DELAY_MAX = float(os.getenv("READ_DELAY_MAX", default=_yaml_get("delay", "read_max", default="2.0")))
|
||||||
|
TYPING_SPEED = float(os.getenv("TYPING_SPEED", default=_yaml_get("delay", "typing_speed", default="15.0")))
|
||||||
|
TYPING_MAX = float(os.getenv("TYPING_MAX", default=_yaml_get("delay", "typing_max", default="10.0")))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Character Preset Override ──────────────────────────────────────────────────
|
||||||
|
# Jika AGENT_CHARACTER di-set, baca character.md dari agent/characters/<preset>/
|
||||||
|
# dan override nilai persona yang relevan.
|
||||||
|
|
||||||
|
ENV_CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
|
||||||
|
ENV_CHARACTER_CONFIG_PATH = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "character.md" if AGENT_CHARACTER else None
|
||||||
|
|
||||||
|
if ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
|
||||||
|
_character_env: dict[str, str] = {}
|
||||||
|
for line in ENV_CHARACTER_CONFIG_PATH.read_text(encoding="utf-8").splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
@ -92,14 +124,3 @@ if CHARACTER_CONFIG_PATH and CHARACTER_CONFIG_PATH.is_file():
|
|||||||
for key, value in _character_overrides.items():
|
for key, value in _character_overrides.items():
|
||||||
globals()[key] = value
|
globals()[key] = value
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
# Selective response: true = roleplayer hanya respon kalau ada mention/relevansi (default).
|
|
||||||
# false = roleplayer semua pesan ikut respon (seperti biasa, tanpa filter).
|
|
||||||
XMPP_SELECTIVE_RESPONSE = os.getenv("XMPP_SELECTIVE_RESPONSE", default="true").strip().lower() in ("true", "1", "yes")
|
|
||||||
|
|
||||||
# Humanize Delay Configuration (anti-bot detection)
|
|
||||||
READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (second)
|
|
||||||
READ_DELAY_MAX = float( os.getenv("READ_DELAY_MAX", default="2.0" ) ) # max reading delay (second)
|
|
||||||
TYPING_SPEED = float( os.getenv("TYPING_SPEED", default="15.0" ) ) # characters per second
|
|
||||||
TYPING_MAX = float( os.getenv("TYPING_MAX", default="10.0" ) ) # max typing delay limit (second)
|
|
||||||
|
|
||||||
|
|||||||
27
config.yaml
Normal file
27
config.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Agent behavior
|
||||||
|
agent:
|
||||||
|
max_iterations: 30
|
||||||
|
max_tool_output: 40000
|
||||||
|
|
||||||
|
# Character & Skills
|
||||||
|
character:
|
||||||
|
preset: hendrik # nama directory di agent/characters/<preset>/
|
||||||
|
skills: "" # comma-separated, e.g. "programmer,analyst"
|
||||||
|
|
||||||
|
# XMPP
|
||||||
|
xmpp:
|
||||||
|
enabled: false
|
||||||
|
muc_rooms: "" # comma-separated, e.g. "room1@conference.server,room2@conference.server"
|
||||||
|
nickname: "" # custom nick MUC (kosong = pakai username)
|
||||||
|
selective_response: true # true = hanya respon kalau ada mention/relevansi
|
||||||
|
|
||||||
|
# Humanize Delay (anti-bot detection)
|
||||||
|
delay:
|
||||||
|
read_min: 1.0 # min reading delay (detik)
|
||||||
|
read_max: 2.0 # max reading delay (detik)
|
||||||
|
typing_speed: 15.0 # characters per second
|
||||||
|
typing_max: 10.0 # max typing delay limit (detik)
|
||||||
|
|
||||||
|
# RAG
|
||||||
|
rag:
|
||||||
|
persist_dir: chroma_db # ChromaDB ONNX default (all-MiniLM-L6-v2, lokal)
|
||||||
@ -1,4 +1,5 @@
|
|||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
|
PyYAML>=6.0
|
||||||
chromadb>=0.5.0
|
chromadb>=0.5.0
|
||||||
openpyxl>=3.1.0
|
openpyxl>=3.1.0
|
||||||
slixmpp
|
slixmpp
|
||||||
|
|||||||
@ -32,9 +32,8 @@ class LLMClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
||||||
response = json.loads(resp.read().decode('utf-8'))
|
raw = resp.read().decode('utf-8')
|
||||||
message = response['choices'][0]['message']
|
response = json.loads(raw)
|
||||||
return self.Message(message)
|
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
body_text = ""
|
body_text = ""
|
||||||
try:
|
try:
|
||||||
@ -54,3 +53,40 @@ class LLMClient:
|
|||||||
return self.Message({'content': f"HTTP Error: {e.code} {e.reason}{detail}", 'tool_calls': None})
|
return self.Message({'content': f"HTTP Error: {e.code} {e.reason}{detail}", 'tool_calls': None})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.Message({'content': f"Error: {str(e)}", 'tool_calls': None})
|
return self.Message({'content': f"Error: {str(e)}", 'tool_calls': None})
|
||||||
|
|
||||||
|
if 'choices' not in response:
|
||||||
|
raw_preview = json.dumps(response)[:500]
|
||||||
|
return self.Message({
|
||||||
|
'content': (
|
||||||
|
f"Error: Unexpected response — 'choices' key missing.\n"
|
||||||
|
f" URL : {url}\n"
|
||||||
|
f" Model : {self.model}\n"
|
||||||
|
f" Response: {raw_preview}"
|
||||||
|
),
|
||||||
|
'tool_calls': None
|
||||||
|
})
|
||||||
|
if not response['choices']:
|
||||||
|
raw_preview = json.dumps(response)[:500]
|
||||||
|
return self.Message({
|
||||||
|
'content': (
|
||||||
|
f"Error: 'choices' is empty in the response.\n"
|
||||||
|
f" URL : {url}\n"
|
||||||
|
f" Model : {self.model}\n"
|
||||||
|
f" Response: {raw_preview}"
|
||||||
|
),
|
||||||
|
'tool_calls': None
|
||||||
|
})
|
||||||
|
if 'message' not in response['choices'][0]:
|
||||||
|
raw_preview = json.dumps(response['choices'][0])[:500]
|
||||||
|
return self.Message({
|
||||||
|
'content': (
|
||||||
|
f"Error: 'message' key missing in first choice.\n"
|
||||||
|
f" URL : {url}\n"
|
||||||
|
f" Model : {self.model}\n"
|
||||||
|
f" Choice : {raw_preview}"
|
||||||
|
),
|
||||||
|
'tool_calls': None
|
||||||
|
})
|
||||||
|
|
||||||
|
message = response['choices'][0]['message']
|
||||||
|
return self.Message(message)
|
||||||
|
|||||||
@ -1,65 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Persona & System Prompt Builder
|
||||||
|
|
||||||
|
Arsitektur:
|
||||||
|
1. Base System Prompt → instruksi inti (tools, RAG, response format)
|
||||||
|
2. Env Character → persona (identity, communication style, description)
|
||||||
|
→ policies (git policy, safety rules)
|
||||||
|
3. Skills → role-specific instructions (programmer, roleplayer, analyst)
|
||||||
|
|
||||||
|
Load order: Base → Character → Policies → Skills
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# ─── Mode / Skill ────────────────────────────────────────────────────────────
|
# ─── Paths ────────────────────────────────────────────────────────────────────
|
||||||
# Pilihan mode AI:
|
|
||||||
# - "programmer" : AI Agent untuk koding (default), tool-focused, task-oriented
|
BASE_DIR = Path(__file__).resolve().parent.parent / "agent"
|
||||||
# - "roleplayer" : Teman ngobrol / chat companion, conversational, expressive
|
BASE_PROMPT_PATH = BASE_DIR / "base-system-prompt.md"
|
||||||
|
ENV_CHARACTERS_DIR = BASE_DIR / "characters"
|
||||||
|
SKILLS_DIR = BASE_DIR / "skills"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Mode / Skill ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
|
MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
|
||||||
|
|
||||||
|
|
||||||
# ─── Personality Configuration ───────────────────────────────────────────────
|
# ─── Personality Configuration ────────────────────────────────────────────────
|
||||||
# Semua parameter personality bisa di-set via .env.
|
|
||||||
# Lihat komentar di setiap field untuk pilihan yang tersedia.
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PersonalityConfig:
|
class PersonalityConfig:
|
||||||
"""Konfigurasi personality AI yang berlaku lintas mode."""
|
"""Konfigurasi personality AI yang berlaku lintas mode."""
|
||||||
|
|
||||||
# Nama panggilan AI (default: "OWL")
|
|
||||||
name: str = "OWL"
|
name: str = "OWL"
|
||||||
|
|
||||||
# Umur persona AI (opsional; kosong bila tidak ingin ditampilkan)
|
|
||||||
age: str = ""
|
age: str = ""
|
||||||
|
|
||||||
# Jenis kelamin persona AI (opsional; kosong bila tidak ingin ditampilkan)
|
|
||||||
gender: str = ""
|
gender: str = ""
|
||||||
|
|
||||||
# Gaya bicara:
|
|
||||||
# "casual" → santai, gaul
|
|
||||||
# "formal" → sopan, profesional
|
|
||||||
# "playful" → ceria, suka bercanda
|
|
||||||
# "warm" → hangat, ramah
|
|
||||||
tone: str = "casual"
|
tone: str = "casual"
|
||||||
|
|
||||||
# Panjang jawaban:
|
|
||||||
# "concise" → singkat, to the point
|
|
||||||
# "balanced" → sedang (default)
|
|
||||||
# "detailed" → panjang, detail
|
|
||||||
verbosity: str = "balanced"
|
verbosity: str = "balanced"
|
||||||
|
|
||||||
# Seberapa sering bercanda:
|
|
||||||
# "none" → serius, tidak bercanda
|
|
||||||
# "light" → sesekali (default)
|
|
||||||
# "witty" → sering, jenaka
|
|
||||||
humor_level: str = "light"
|
humor_level: str = "light"
|
||||||
|
|
||||||
# Bahasa utama:
|
|
||||||
# "id" → Indonesia
|
|
||||||
# "en" → English
|
|
||||||
# "" → auto (sesuai bahasa user)
|
|
||||||
language: str = "id"
|
language: str = "id"
|
||||||
|
|
||||||
# Suasana hati umum:
|
|
||||||
# "cheerful" → ceria, positif
|
|
||||||
# "calm" → tenang, menenangkan
|
|
||||||
# "energetic" → bersemangat, aktif
|
|
||||||
# "sarcastic" → sarkastik, sinis
|
|
||||||
mood: str = "cheerful"
|
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)
|
catchphrases: list = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@ -81,11 +64,53 @@ def _load_personality_from_env() -> PersonalityConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Instance global — di-load sekali saat import
|
|
||||||
PERSONALITY = _load_personality_from_env()
|
PERSONALITY = _load_personality_from_env()
|
||||||
|
|
||||||
|
|
||||||
# ─── Prompt Builders ─────────────────────────────────────────────────────────
|
# ─── Markdown Parser ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _parse_simple_kv(filepath: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Parse file markdown dengan format sederhana:
|
||||||
|
# Section
|
||||||
|
- **Key:** Value
|
||||||
|
- Key: Value
|
||||||
|
|
||||||
|
Returns dict { key: value }.
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
if not filepath.exists():
|
||||||
|
return result
|
||||||
|
|
||||||
|
for line in filepath.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
# Format: "- **Key:** Value" atau "- Key: Value"
|
||||||
|
cleaned = re.sub(r'^-\s*', '', line)
|
||||||
|
cleaned = re.sub(r'\*\*', '', cleaned)
|
||||||
|
if ':' in cleaned:
|
||||||
|
key, value = cleaned.split(':', 1)
|
||||||
|
result[key.strip().lower()] = value.strip()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _read_markdown_section(filepath: Path) -> str:
|
||||||
|
"""Baca seluruh isi file markdown, stripping frontmatter jika ada."""
|
||||||
|
if not filepath.exists():
|
||||||
|
return ""
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
# Strip leading --- frontmatter blocks
|
||||||
|
content = re.sub(r'^---\s*\n.*?\n---\s*\n', '', content, flags=re.DOTALL)
|
||||||
|
# Strip leading # title if present (first line only)
|
||||||
|
lines = content.strip().splitlines()
|
||||||
|
if lines and lines[0].startswith('# '):
|
||||||
|
lines = lines[1:]
|
||||||
|
return '\n'.join(lines).strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Prompt Builders ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_personality_block(cfg: PersonalityConfig) -> str:
|
def _build_personality_block(cfg: PersonalityConfig) -> str:
|
||||||
"""Generate deskripsi personality dari config."""
|
"""Generate deskripsi personality dari config."""
|
||||||
@ -97,7 +122,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
if cfg.gender:
|
if cfg.gender:
|
||||||
parts.append(f"Your persona gender is {cfg.gender}.")
|
parts.append(f"Your persona gender is {cfg.gender}.")
|
||||||
|
|
||||||
# Tone
|
|
||||||
tone_map = {
|
tone_map = {
|
||||||
"casual": "You speak in a casual, relaxed manner — like chatting with a friend.",
|
"casual": "You speak in a casual, relaxed manner — like chatting with a friend.",
|
||||||
"formal": "You speak formally and professionally, using polite language.",
|
"formal": "You speak formally and professionally, using polite language.",
|
||||||
@ -106,7 +130,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
}
|
}
|
||||||
parts.append(tone_map.get(cfg.tone, tone_map["casual"]))
|
parts.append(tone_map.get(cfg.tone, tone_map["casual"]))
|
||||||
|
|
||||||
# Verbosity
|
|
||||||
verbosity_map = {
|
verbosity_map = {
|
||||||
"concise": "Keep your answers short and to the point.",
|
"concise": "Keep your answers short and to the point.",
|
||||||
"balanced": "Provide balanced answers — not too brief, not too long.",
|
"balanced": "Provide balanced answers — not too brief, not too long.",
|
||||||
@ -114,7 +137,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
}
|
}
|
||||||
parts.append(verbosity_map.get(cfg.verbosity, verbosity_map["balanced"]))
|
parts.append(verbosity_map.get(cfg.verbosity, verbosity_map["balanced"]))
|
||||||
|
|
||||||
# Humor
|
|
||||||
humor_map = {
|
humor_map = {
|
||||||
"none": "Stay serious and avoid jokes.",
|
"none": "Stay serious and avoid jokes.",
|
||||||
"light": "Occasionally sprinkle in light humor when appropriate.",
|
"light": "Occasionally sprinkle in light humor when appropriate.",
|
||||||
@ -122,7 +144,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
}
|
}
|
||||||
parts.append(humor_map.get(cfg.humor_level, humor_map["light"]))
|
parts.append(humor_map.get(cfg.humor_level, humor_map["light"]))
|
||||||
|
|
||||||
# Language
|
|
||||||
if cfg.language == "id":
|
if cfg.language == "id":
|
||||||
parts.append("Always respond in Indonesian (Bahasa Indonesia).")
|
parts.append("Always respond in Indonesian (Bahasa Indonesia).")
|
||||||
elif cfg.language == "en":
|
elif cfg.language == "en":
|
||||||
@ -130,7 +151,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
else:
|
else:
|
||||||
parts.append("Respond in the same language the user uses.")
|
parts.append("Respond in the same language the user uses.")
|
||||||
|
|
||||||
# Mood
|
|
||||||
mood_map = {
|
mood_map = {
|
||||||
"cheerful": "Your overall mood is cheerful and positive.",
|
"cheerful": "Your overall mood is cheerful and positive.",
|
||||||
"calm": "Your overall mood is calm and soothing.",
|
"calm": "Your overall mood is calm and soothing.",
|
||||||
@ -139,7 +159,6 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
}
|
}
|
||||||
parts.append(mood_map.get(cfg.mood, mood_map["cheerful"]))
|
parts.append(mood_map.get(cfg.mood, mood_map["cheerful"]))
|
||||||
|
|
||||||
# Catchphrases
|
|
||||||
if cfg.catchphrases:
|
if cfg.catchphrases:
|
||||||
phrases = ", ".join(f'"{p}"' for p in cfg.catchphrases)
|
phrases = ", ".join(f'"{p}"' for p in cfg.catchphrases)
|
||||||
parts.append(f"You sometimes use these catchphrases: {phrases}.")
|
parts.append(f"You sometimes use these catchphrases: {phrases}.")
|
||||||
@ -166,104 +185,69 @@ def _build_tools_block(tools_definition: list[dict]) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _build_programmer_prompt(cfg: PersonalityConfig, tools_definition: list[dict]) -> str:
|
def _load_env_character(character_name: str) -> tuple[str, str]:
|
||||||
"""Build system prompt untuk mode Programmer."""
|
"""
|
||||||
lines = [
|
Load character description dan policies dari directory env-characters.
|
||||||
_build_personality_block(cfg),
|
|
||||||
"",
|
Returns:
|
||||||
"You are a coding agent that assists with software engineering tasks.",
|
(character_block, policies_text)
|
||||||
"",
|
"""
|
||||||
_build_tools_block(tools_definition),
|
char_dir = ENV_CHARACTERS_DIR / character_name
|
||||||
"",
|
if not char_dir.is_dir():
|
||||||
f"Your workspace directory is: {os.getcwd()}. "
|
return "", ""
|
||||||
"All file operations are relative to this directory.",
|
|
||||||
"",
|
# Read character.md — derive personality from config (already loaded)
|
||||||
"⚠️ GIT POLICY — IMPORTANT:",
|
character_block = "" # personality block tetap dari PersonalityConfig
|
||||||
"- NEVER run 'git add' or 'git commit' automatically after making changes.",
|
|
||||||
"- After editing/creating files, always ASK the user first before committing.",
|
# Read policies.md
|
||||||
"- Only run git commands when the user explicitly asks you to commit.",
|
policies_text = _read_markdown_section(char_dir / "policies.md")
|
||||||
"- 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.",
|
return character_block, policies_text
|
||||||
"",
|
|
||||||
"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:
|
def _load_skills(skill_names: list[str]) -> str:
|
||||||
"""Build system prompt untuk mode Roleplayer."""
|
"""
|
||||||
lines = [
|
Load dan gabungkan skill instructions.
|
||||||
_build_personality_block(cfg),
|
|
||||||
"",
|
Args:
|
||||||
f"You are {cfg.name}, a conversational companion and roleplayer. "
|
skill_names: List nama skill aktif, e.g. ["programmer"]
|
||||||
"Your main purpose is to be an engaging, empathetic, and fun conversation partner.",
|
|
||||||
"",
|
Returns:
|
||||||
"Guidelines:",
|
Gabungan skill instructions sebagai string.
|
||||||
"- Stay in character at all times. Be consistent with your personality.",
|
"""
|
||||||
"- Be responsive and empathetic — acknowledge the user's feelings and thoughts.",
|
sections = []
|
||||||
"- Ask follow-up questions to keep the conversation flowing naturally.",
|
for skill_name in skill_names:
|
||||||
"- Use natural, conversational language — not robotic or overly formal.",
|
skill_path = SKILLS_DIR / skill_name / "instructions.md"
|
||||||
"- If the user wants to roleplay a scenario, dive into it enthusiastically.",
|
content = _read_markdown_section(skill_path)
|
||||||
"- Adapt your tone and energy to match the mood of the conversation.",
|
if content:
|
||||||
"- Keep the conversation comfortable and enjoyable.",
|
sections.append(content)
|
||||||
"",
|
return "\n\n".join(sections)
|
||||||
"⚠️ 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 ──────────────────────────────────────────────────────────────
|
# ─── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def build_system_prompt(
|
def build_system_prompt(
|
||||||
tools_definition: list[dict] | None = None,
|
tools_definition: list[dict] | None = None,
|
||||||
mode: str | None = None,
|
mode: str | None = None,
|
||||||
personality: PersonalityConfig | None = None,
|
personality: PersonalityConfig | None = None,
|
||||||
|
character: str | None = None,
|
||||||
|
skills: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Build system prompt berdasarkan mode dan personality config.
|
Build system prompt berdasarkan mode, character, dan skills.
|
||||||
|
|
||||||
|
Load order:
|
||||||
|
1. Base prompt
|
||||||
|
2. Personality block (dari config)
|
||||||
|
3. Policies (dari env-characters/<name>/policies.md)
|
||||||
|
4. Skill instructions (dari skills/<name>/instructions.md)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tools_definition: Daftar tools (required untuk mode programmer).
|
tools_definition: Daftar tools (required untuk skill programmer).
|
||||||
mode: "programmer" atau "roleplayer". Default: dari env PERSONA_MODE.
|
mode: "programmer" atau "roleplayer". Default: dari env PERSONA_MODE.
|
||||||
personality: PersonalityConfig instance. Default: dari env (global PERSONALITY).
|
personality: PersonalityConfig instance. Default: global PERSONALITY.
|
||||||
|
character: Nama env character. Default: dari env AGENT_CHARACTER.
|
||||||
|
skills: List nama skill aktif. Default: derives from mode atau env AGENT_SKILLS.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
String system prompt lengkap.
|
String system prompt lengkap.
|
||||||
@ -271,14 +255,50 @@ def build_system_prompt(
|
|||||||
selected_mode = (mode or MODE).strip().lower()
|
selected_mode = (mode or MODE).strip().lower()
|
||||||
cfg = personality or PERSONALITY
|
cfg = personality or PERSONALITY
|
||||||
|
|
||||||
if selected_mode == "programmer":
|
# Resolve character name
|
||||||
if tools_definition is None:
|
character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower()
|
||||||
raise ValueError("tools_definition is required for 'programmer' mode")
|
|
||||||
return _build_programmer_prompt(cfg, tools_definition)
|
# Resolve skills list
|
||||||
elif selected_mode == "roleplayer":
|
if skills is None:
|
||||||
return _build_roleplayer_prompt(cfg)
|
skills_env = os.getenv("AGENT_SKILLS", default="").strip()
|
||||||
|
if skills_env:
|
||||||
|
skills_list = [s.strip() for s in skills_env.split(",") if s.strip()]
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
# Derive from mode untuk backward compatibility
|
||||||
f"Unknown mode: '{selected_mode}'. "
|
skills_list = [selected_mode] if selected_mode in ("programmer", "roleplayer") else []
|
||||||
f"Available modes: 'programmer', 'roleplayer'"
|
else:
|
||||||
)
|
skills_list = skills
|
||||||
|
|
||||||
|
# ── 1. Base prompt ──────────────────────────────────────────────────────────
|
||||||
|
base_prompt = ""
|
||||||
|
if BASE_PROMPT_PATH.exists():
|
||||||
|
base_prompt = _read_markdown_section(BASE_PROMPT_PATH)
|
||||||
|
|
||||||
|
# ── 2. Personality block ────────────────────────────────────────────────────
|
||||||
|
personality_block = _build_personality_block(cfg)
|
||||||
|
|
||||||
|
# ── 3. Tools block (hanya untuk skill yang butuh tools) ─────────────────────
|
||||||
|
needs_tools = any(s in ("programmer", "analyst") for s in skills_list)
|
||||||
|
tools_block = ""
|
||||||
|
if needs_tools and tools_definition is not None:
|
||||||
|
tools_block = _build_tools_block(tools_definition)
|
||||||
|
|
||||||
|
# ── 4. Policies ──────────────────────────────────────────────────────────────
|
||||||
|
policies_block = ""
|
||||||
|
if character_name:
|
||||||
|
_, policies_block = _load_env_character(character_name)
|
||||||
|
|
||||||
|
# ── 5. Skills ────────────────────────────────────────────────────────────────
|
||||||
|
skills_block = _load_skills(skills_list)
|
||||||
|
|
||||||
|
# ── Assemble ─────────────────────────────────────────────────────────────────
|
||||||
|
sections = [
|
||||||
|
base_prompt,
|
||||||
|
personality_block,
|
||||||
|
tools_block,
|
||||||
|
policies_block,
|
||||||
|
skills_block,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Filter empty sections dan gabungkan
|
||||||
|
return "\n\n".join(s for s in sections if s.strip())
|
||||||
|
|||||||
@ -281,7 +281,11 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
def _process_dm(self, jid, body):
|
def _process_dm(self, jid, body):
|
||||||
session = self._session_mgr.get_or_create(
|
session = self._session_mgr.get_or_create(
|
||||||
jid, self._build_system_prompt(tools_definition=self._tools_def)
|
jid, self._build_system_prompt(
|
||||||
|
tools_definition=self._tools_def,
|
||||||
|
character=config.AGENT_CHARACTER or None,
|
||||||
|
skills=config.AGENT_SKILLS.split(",") if config.AGENT_SKILLS else None,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
session.cancel_timer()
|
session.cancel_timer()
|
||||||
|
|
||||||
@ -308,7 +312,11 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
def _process_muc(self, room, nick, body):
|
def _process_muc(self, room, nick, body):
|
||||||
session = self._session_mgr.get_or_create(
|
session = self._session_mgr.get_or_create(
|
||||||
room, self._build_system_prompt(tools_definition=self._tools_def)
|
room, self._build_system_prompt(
|
||||||
|
tools_definition=self._tools_def,
|
||||||
|
character=config.AGENT_CHARACTER or None,
|
||||||
|
skills=config.AGENT_SKILLS.split(",") if config.AGENT_SKILLS else None,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
session.cancel_timer()
|
session.cancel_timer()
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import curses
|
import curses
|
||||||
import threading
|
import threading
|
||||||
|
import config
|
||||||
from .render import init_colors, draw
|
from .render import init_colors, draw
|
||||||
from .input import handle_key
|
from .input import handle_key
|
||||||
from .agent import log, WELCOME_ART
|
from .agent import log, WELCOME_ART
|
||||||
@ -40,7 +41,11 @@ class HendrikTUI:
|
|||||||
stdscr.keypad(True)
|
stdscr.keypad(True)
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
|
|
||||||
self.messages = [{"role": "system", "content": self.build_system_prompt(tools_definition=self.tools_def)}]
|
self.messages = [{"role": "system", "content": self.build_system_prompt(
|
||||||
|
tools_definition=self.tools_def,
|
||||||
|
character=config.AGENT_CHARACTER or None,
|
||||||
|
skills=config.AGENT_SKILLS.split(",") if config.AGENT_SKILLS else None,
|
||||||
|
)}]
|
||||||
log(self, "welcome", WELCOME_ART)
|
log(self, "welcome", WELCOME_ART)
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user