Compare commits
3 Commits
419f2832c8
...
6725b7bf0c
| Author | SHA1 | Date | |
|---|---|---|---|
| 6725b7bf0c | |||
| b12bd9cf2a | |||
| 94c364798f |
74
config.py
74
config.py
@ -80,15 +80,15 @@ AGENT_MAX_ITERATIONS = int(os.getenv("AGENT_MAX_ITERATIONS", default=_yaml_get
|
|||||||
AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get("agent", "max_tool_output", default="40000")))
|
AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get("agent", "max_tool_output", default="40000")))
|
||||||
|
|
||||||
AGENT_SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower()
|
AGENT_SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower()
|
||||||
PERSONA_NAME = os.getenv("PERSONA_NAME", default=_yaml_get("persona", "name", default="Hendrik")).strip() or "Hendrik"
|
PERSONALITY_NAME = os.getenv("PERSONALITY_NAME", default=_yaml_get("personality", "name", default="Hendrik")).strip() or "Hendrik"
|
||||||
PERSONA_AGE = os.getenv("PERSONA_AGE", default=_yaml_get("persona", "age", default="")).strip()
|
PERSONALITY_AGE = os.getenv("PERSONALITY_AGE", default=_yaml_get("personality", "age", default="")).strip()
|
||||||
PERSONA_GENDER = os.getenv("PERSONA_GENDER", default=_yaml_get("persona", "gender", default="")).strip()
|
PERSONALITY_GENDER = os.getenv("PERSONALITY_GENDER", default=_yaml_get("personality", "gender", default="")).strip()
|
||||||
PERSONA_TONE = os.getenv("PERSONA_TONE", default=_yaml_get("persona", "tone", default="casual")).strip().lower() or "casual"
|
PERSONALITY_TONE = os.getenv("PERSONALITY_TONE", default=_yaml_get("personality", "tone", default="casual")).strip().lower() or "casual"
|
||||||
PERSONA_VERBOSITY = os.getenv("PERSONA_VERBOSITY", default=_yaml_get("persona", "verbosity", default="balanced")).strip().lower() or "balanced"
|
PERSONALITY_VERBOSITY = os.getenv("PERSONALITY_VERBOSITY", default=_yaml_get("personality", "verbosity", default="balanced")).strip().lower() or "balanced"
|
||||||
PERSONA_HUMOR = os.getenv("PERSONA_HUMOR", default=_yaml_get("persona", "humor", default="light")).strip().lower() or "light"
|
PERSONALITY_HUMOR = os.getenv("PERSONALITY_HUMOR", default=_yaml_get("personality", "humor", default="light")).strip().lower() or "light"
|
||||||
PERSONA_LANGUAGE = os.getenv("PERSONA_LANGUAGE", default=_yaml_get("persona", "language", default="id")).strip().lower() or "id"
|
PERSONALITY_LANGUAGE = os.getenv("PERSONALITY_LANGUAGE", default=_yaml_get("personality", "language", default="id")).strip().lower() or "id"
|
||||||
PERSONA_MOOD = os.getenv("PERSONA_MOOD", default=_yaml_get("persona", "mood", default="cheerful")).strip().lower() or "cheerful"
|
PERSONALITY_MOOD = os.getenv("PERSONALITY_MOOD", default=_yaml_get("personality", "mood", default="cheerful")).strip().lower() or "cheerful"
|
||||||
PERSONA_CATCHPHRASES = os.getenv("PERSONA_CATCHPHRASES", default=_yaml_get("persona", "catchphrases", default="")).strip()
|
PERSONALITY_CATCHPHRASES = os.getenv("PERSONALITY_CATCHPHRASES", default=_yaml_get("personality", "catchphrases", default="")).strip()
|
||||||
|
|
||||||
|
|
||||||
# ─── Character & Skills (YAML, bisa di-override dari .env) ─────────────────────
|
# ─── Character & Skills (YAML, bisa di-override dari .env) ─────────────────────
|
||||||
@ -135,22 +135,22 @@ TYPING_MAX = float(os.getenv("TYPING_MAX", default=_yaml_get("delay", "t
|
|||||||
|
|
||||||
# ─── Character Preset Override ──────────────────────────────────────────────────
|
# ─── Character Preset Override ──────────────────────────────────────────────────
|
||||||
# Jika AGENT_CHARACTER di-set, baca character.md dari agent/characters/<preset>/
|
# Jika AGENT_CHARACTER di-set, baca character.md dari agent/characters/<preset>/
|
||||||
# dan override nilai persona yang relevan.
|
# dan override nilai personality yang relevan.
|
||||||
|
|
||||||
ENV_CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
|
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
|
ENV_CHARACTER_CONFIG_PATH = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "character.md" if AGENT_CHARACTER else None
|
||||||
|
|
||||||
# Coba persona.yaml dulu (prioritas utama), kalau tidak ada fallback ke character.md
|
# Coba personality.yaml dulu (prioritas utama), kalau tidak ada fallback ke character.md
|
||||||
_persona_yaml_path = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "persona.yaml" if AGENT_CHARACTER else None
|
_personality_yaml_path = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "personality.yaml" if AGENT_CHARACTER else None
|
||||||
|
|
||||||
if _persona_yaml_path and _persona_yaml_path.is_file():
|
if _personality_yaml_path and _personality_yaml_path.is_file():
|
||||||
# Prioritas utama: baca persona.yaml dari character directory
|
# Prioritas utama: baca personality.yaml dari character directory
|
||||||
try:
|
try:
|
||||||
_character_env = yaml.safe_load(_persona_yaml_path.read_text(encoding="utf-8")) or {}
|
_character_env = yaml.safe_load(_personality_yaml_path.read_text(encoding="utf-8")) or {}
|
||||||
if not isinstance(_character_env, dict):
|
if not isinstance(_character_env, dict):
|
||||||
_character_env = {}
|
_character_env = {}
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
print(f"[config] Warning: gagal load persona.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
|
print(f"[config] Warning: gagal load personality.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
|
||||||
_character_env = {}
|
_character_env = {}
|
||||||
elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
|
elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
|
||||||
# Fallback: baca character.md (format lama)
|
# Fallback: baca character.md (format lama)
|
||||||
@ -169,36 +169,36 @@ if _character_env:
|
|||||||
|
|
||||||
_character_overrides = {
|
_character_overrides = {
|
||||||
"AGENT_SKILL": AGENT_SKILL,
|
"AGENT_SKILL": AGENT_SKILL,
|
||||||
"PERSONA_NAME": PERSONA_NAME,
|
"PERSONALITY_NAME": PERSONALITY_NAME,
|
||||||
"PERSONA_AGE": PERSONA_AGE,
|
"PERSONALITY_AGE": PERSONALITY_AGE,
|
||||||
"PERSONA_GENDER": PERSONA_GENDER,
|
"PERSONALITY_GENDER": PERSONALITY_GENDER,
|
||||||
"PERSONA_TONE": PERSONA_TONE,
|
"PERSONALITY_TONE": PERSONALITY_TONE,
|
||||||
"PERSONA_VERBOSITY": PERSONA_VERBOSITY,
|
"PERSONALITY_VERBOSITY": PERSONALITY_VERBOSITY,
|
||||||
"PERSONA_HUMOR": PERSONA_HUMOR,
|
"PERSONALITY_HUMOR": PERSONALITY_HUMOR,
|
||||||
"PERSONA_LANGUAGE": PERSONA_LANGUAGE,
|
"PERSONALITY_LANGUAGE": PERSONALITY_LANGUAGE,
|
||||||
"PERSONA_MOOD": PERSONA_MOOD,
|
"PERSONALITY_MOOD": PERSONALITY_MOOD,
|
||||||
"PERSONA_CATCHPHRASES": PERSONA_CATCHPHRASES,
|
"PERSONALITY_CATCHPHRASES": PERSONALITY_CATCHPHRASES,
|
||||||
}
|
}
|
||||||
# Mapping dari key persona.yaml ke key config
|
# Mapping dari key personality.yaml ke key config
|
||||||
_yaml_to_config_key = {
|
_yaml_to_config_key = {
|
||||||
"AGENT_SKILL": "skill",
|
"AGENT_SKILL": "skill",
|
||||||
"PERSONA_NAME": "name",
|
"PERSONALITY_NAME": "name",
|
||||||
"PERSONA_AGE": "age",
|
"PERSONALITY_AGE": "age",
|
||||||
"PERSONA_GENDER": "gender",
|
"PERSONALITY_GENDER": "gender",
|
||||||
"PERSONA_TONE": "tone",
|
"PERSONALITY_TONE": "tone",
|
||||||
"PERSONA_VERBOSITY": "verbosity",
|
"PERSONALITY_VERBOSITY": "verbosity",
|
||||||
"PERSONA_HUMOR": "humor",
|
"PERSONALITY_HUMOR": "humor",
|
||||||
"PERSONA_LANGUAGE": "language",
|
"PERSONALITY_LANGUAGE": "language",
|
||||||
"PERSONA_MOOD": "mood",
|
"PERSONALITY_MOOD": "mood",
|
||||||
"PERSONA_CATCHPHRASES": "catchphrases",
|
"PERSONALITY_CATCHPHRASES": "catchphrases",
|
||||||
}
|
}
|
||||||
for key, fallback in _character_overrides.items():
|
for key, fallback in _character_overrides.items():
|
||||||
yaml_key = _yaml_to_config_key.get(key, key.lower())
|
yaml_key = _yaml_to_config_key.get(key, key.lower())
|
||||||
raw = _character_env.get(yaml_key, _character_env.get(key, fallback))
|
raw = _character_env.get(yaml_key, _character_env.get(key, fallback))
|
||||||
value = str(raw).strip() if raw is not None else ""
|
value = str(raw).strip() if raw is not None else ""
|
||||||
if key in {"AGENT_SKILL", "PERSONA_TONE", "PERSONA_VERBOSITY", "PERSONA_HUMOR", "PERSONA_LANGUAGE", "PERSONA_MOOD"}:
|
if key in {"AGENT_SKILL", "PERSONALITY_TONE", "PERSONALITY_VERBOSITY", "PERSONALITY_HUMOR", "PERSONALITY_LANGUAGE", "PERSONALITY_MOOD"}:
|
||||||
value = value.lower() or fallback
|
value = value.lower() or fallback
|
||||||
if key == "PERSONA_NAME" and not value:
|
if key == "PERSONALITY_NAME" and not value:
|
||||||
value = fallback
|
value = fallback
|
||||||
_character_overrides[key] = value
|
_character_overrides[key] = value
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from services.xmpp_client import XMPPClient
|
|||||||
from scripts.llm_client import LLMClient
|
from scripts.llm_client import LLMClient
|
||||||
from tools import coder, rag, carrack
|
from tools import coder, rag, carrack
|
||||||
from scripts import gadget
|
from scripts import gadget
|
||||||
from scripts.persona import build_system_prompt
|
from scripts.personality import build_system_prompt
|
||||||
|
|
||||||
tools_definition = [
|
tools_definition = [
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from .persona import build_system_prompt
|
from .personality import build_system_prompt
|
||||||
|
|
||||||
def tools_mapping(schema, handler, name=None):
|
def tools_mapping(schema, handler, name=None):
|
||||||
tool_name = name or schema["function"]["name"]
|
tool_name = name or schema["function"]["name"]
|
||||||
|
|||||||
@ -52,12 +52,14 @@ class LLMClient:
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
self.cancel_requested = False
|
||||||
|
|
||||||
def chat(self, messages, tools=None):
|
def chat(self, messages, tools=None, on_stream_chunk=None):
|
||||||
url = f"{self.base_url}/chat/completions"
|
url = f"{self.base_url}/chat/completions"
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages
|
"messages": messages,
|
||||||
|
"stream": True # Enable streaming
|
||||||
}
|
}
|
||||||
if tools:
|
if tools:
|
||||||
payload["tools"] = tools
|
payload["tools"] = tools
|
||||||
@ -72,10 +74,108 @@ class LLMClient:
|
|||||||
req.add_header('Content-Type', 'application/json')
|
req.add_header('Content-Type', 'application/json')
|
||||||
req.add_header('Authorization', f'Bearer {self.api_key}')
|
req.add_header('Authorization', f'Bearer {self.api_key}')
|
||||||
|
|
||||||
|
# Variabel untuk mengumpulkan hasil
|
||||||
|
full_content = ""
|
||||||
|
full_tool_calls = []
|
||||||
|
reasoning_content = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.cancel_requested = False
|
||||||
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
||||||
raw = resp.read().decode('utf-8')
|
# Streaming: baca line by line
|
||||||
response = json.loads(raw)
|
for line in resp:
|
||||||
|
if self.cancel_requested:
|
||||||
|
# Stream cancellation
|
||||||
|
full_content += "\n\n[Stream cancelled by user]"
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.decode('utf-8').strip()
|
||||||
|
if not line or not line.startswith('data: '):
|
||||||
|
continue
|
||||||
|
|
||||||
|
data_str = line[6:] # Hapus "data: " prefix
|
||||||
|
if data_str == '[DONE]':
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunk = json.loads(data_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse delta dari chunk
|
||||||
|
delta = chunk.get('choices', [{}])[0].get('delta', {})
|
||||||
|
finish_reason = chunk.get('choices', [{}])[0].get('finish_reason', None)
|
||||||
|
|
||||||
|
# Stream reasoning content jika ada
|
||||||
|
if 'reasoning_content' in delta:
|
||||||
|
reasoning_content += delta['reasoning_content']
|
||||||
|
|
||||||
|
# Stream tool_calls jika ada
|
||||||
|
if 'tool_calls' in delta:
|
||||||
|
tool_calls = delta['tool_calls']
|
||||||
|
for tc in tool_calls:
|
||||||
|
idx = tc.get('index', 0)
|
||||||
|
# Pastikan list cukup panjang
|
||||||
|
while len(full_tool_calls) <= idx:
|
||||||
|
full_tool_calls.append({
|
||||||
|
"id": "",
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": "", "arguments": ""}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Update ID
|
||||||
|
if 'id' in tc and tc['id']:
|
||||||
|
full_tool_calls[idx]['id'] = tc['id']
|
||||||
|
|
||||||
|
# Update function name
|
||||||
|
if 'function' in tc and 'name' in tc['function']:
|
||||||
|
full_tool_calls[idx]['function']['name'] += tc['function']['name']
|
||||||
|
|
||||||
|
# Update arguments
|
||||||
|
if 'function' in tc and 'arguments' in tc['function']:
|
||||||
|
full_tool_calls[idx]['function']['arguments'] += tc['function']['arguments']
|
||||||
|
|
||||||
|
# Stream content (text response)
|
||||||
|
if 'content' in delta:
|
||||||
|
chunk_text = delta['content']
|
||||||
|
full_content += chunk_text
|
||||||
|
|
||||||
|
# Callback untuk streaming ke UI
|
||||||
|
if on_stream_chunk:
|
||||||
|
on_stream_chunk(chunk_text)
|
||||||
|
|
||||||
|
# Build final response
|
||||||
|
message = {'content': full_content}
|
||||||
|
|
||||||
|
if full_tool_calls:
|
||||||
|
# Filter tool_calls yang valid (ada name dan arguments)
|
||||||
|
valid_tool_calls = []
|
||||||
|
for tc in full_tool_calls:
|
||||||
|
name = tc.get('function', {}).get('name')
|
||||||
|
args_str = tc.get('function', {}).get('arguments')
|
||||||
|
tc_id = tc.get('id')
|
||||||
|
|
||||||
|
# Pastikan name dan arguments ada
|
||||||
|
if name and args_str and args_str.strip():
|
||||||
|
# Generate ID jika kosong
|
||||||
|
if not tc_id:
|
||||||
|
tc_id = f"call_{len(valid_tool_calls)}"
|
||||||
|
tc['id'] = tc_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Validate dan re-encode JSON untuk format yang konsisten
|
||||||
|
parsed_args = json.loads(args_str)
|
||||||
|
tc['function']['arguments'] = json.dumps(parsed_args, ensure_ascii=False)
|
||||||
|
valid_tool_calls.append(tc)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Invalid JSON, coba raw string tapi hanya jika tidak kosong
|
||||||
|
tc['function']['arguments'] = args_str
|
||||||
|
valid_tool_calls.append(tc)
|
||||||
|
|
||||||
|
if valid_tool_calls:
|
||||||
|
message['tool_calls'] = valid_tool_calls
|
||||||
|
|
||||||
|
response = {'choices': [{'message': message}]}
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
body_text = ""
|
body_text = ""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -50,18 +50,18 @@ class PersonalityConfig:
|
|||||||
|
|
||||||
def _load_personality_from_env() -> PersonalityConfig:
|
def _load_personality_from_env() -> PersonalityConfig:
|
||||||
"""Baca personality config dari environment variables."""
|
"""Baca personality config dari environment variables."""
|
||||||
raw_catchphrases = os.getenv("PERSONA_CATCHPHRASES", default="").strip()
|
raw_catchphrases = os.getenv("PERSONALITY_CATCHPHRASES", default="").strip()
|
||||||
catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else []
|
catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else []
|
||||||
|
|
||||||
return PersonalityConfig(
|
return PersonalityConfig(
|
||||||
name=os.getenv("PERSONA_NAME", default="OWL").strip() or "OWL",
|
name=os.getenv("PERSONALITY_NAME", default="OWL").strip() or "OWL",
|
||||||
age=os.getenv("PERSONA_AGE", default="").strip(),
|
age=os.getenv("PERSONALITY_AGE", default="").strip(),
|
||||||
gender=os.getenv("PERSONA_GENDER", default="").strip(),
|
gender=os.getenv("PERSONALITY_GENDER", default="").strip(),
|
||||||
tone=os.getenv("PERSONA_TONE", default="casual").strip().lower() or "casual",
|
tone=os.getenv("PERSONALITY_TONE", default="casual").strip().lower() or "casual",
|
||||||
verbosity=os.getenv("PERSONA_VERBOSITY", default="balanced").strip().lower() or "balanced",
|
verbosity=os.getenv("PERSONALITY_VERBOSITY", default="balanced").strip().lower() or "balanced",
|
||||||
humor_level=os.getenv("PERSONA_HUMOR", default="light").strip().lower() or "light",
|
humor_level=os.getenv("PERSONALITY_HUMOR", default="light").strip().lower() or "light",
|
||||||
language=os.getenv("PERSONA_LANGUAGE", default="id").strip().lower() or "id",
|
language=os.getenv("PERSONALITY_LANGUAGE", default="id").strip().lower() or "id",
|
||||||
mood=os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or "cheerful",
|
mood=os.getenv("PERSONALITY_MOOD", default="cheerful").strip().lower() or "cheerful",
|
||||||
catchphrases=catchphrases,
|
catchphrases=catchphrases,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -119,10 +119,10 @@ def _build_personality_block(cfg: PersonalityConfig) -> str:
|
|||||||
parts = [f"You are {cfg.name}."]
|
parts = [f"You are {cfg.name}."]
|
||||||
|
|
||||||
if cfg.age:
|
if cfg.age:
|
||||||
parts.append(f"Your persona age is {cfg.age} years old.")
|
parts.append(f"Your personality age is {cfg.age} years old.")
|
||||||
|
|
||||||
if cfg.gender:
|
if cfg.gender:
|
||||||
parts.append(f"Your persona gender is {cfg.gender}.")
|
parts.append(f"Your personality gender is {cfg.gender}.")
|
||||||
|
|
||||||
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.",
|
||||||
@ -261,46 +261,46 @@ def build_system_prompt(
|
|||||||
# Resolve character name
|
# Resolve character name
|
||||||
character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower()
|
character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower()
|
||||||
|
|
||||||
# ── Load persona.yaml dari character directory ─────────────────────────────────
|
# ── Load personality.yaml dari character directory ─────────────────────────────────
|
||||||
if character_name:
|
if character_name:
|
||||||
char_dir = ENV_CHARACTERS_DIR / character_name
|
char_dir = ENV_CHARACTERS_DIR / character_name
|
||||||
persona_yaml_path = char_dir / "persona.yaml"
|
personality_yaml_path = char_dir / "personality.yaml"
|
||||||
if persona_yaml_path.is_file():
|
if personality_yaml_path.is_file():
|
||||||
try:
|
try:
|
||||||
with open(persona_yaml_path, "r", encoding="utf-8") as f:
|
with open(personality_yaml_path, "r", encoding="utf-8") as f:
|
||||||
_persona_data = yaml.safe_load(f) or {}
|
_personality_data = yaml.safe_load(f) or {}
|
||||||
if isinstance(_persona_data, dict):
|
if isinstance(_personality_data, dict):
|
||||||
if _persona_data.get("name"):
|
if _personality_data.get("name"):
|
||||||
cfg.name = _persona_data["name"]
|
cfg.name = _personality_data["name"]
|
||||||
if _persona_data.get("age"):
|
if _personality_data.get("age"):
|
||||||
cfg.age = str(_persona_data["age"])
|
cfg.age = str(_personality_data["age"])
|
||||||
if _persona_data.get("gender"):
|
if _personality_data.get("gender"):
|
||||||
cfg.gender = _persona_data["gender"]
|
cfg.gender = _personality_data["gender"]
|
||||||
if _persona_data.get("tone"):
|
if _personality_data.get("tone"):
|
||||||
cfg.tone = _persona_data["tone"]
|
cfg.tone = _personality_data["tone"]
|
||||||
if _persona_data.get("verbosity"):
|
if _personality_data.get("verbosity"):
|
||||||
cfg.verbosity = _persona_data["verbosity"]
|
cfg.verbosity = _personality_data["verbosity"]
|
||||||
if _persona_data.get("humor"):
|
if _personality_data.get("humor"):
|
||||||
cfg.humor_level = _persona_data["humor"]
|
cfg.humor_level = _personality_data["humor"]
|
||||||
if _persona_data.get("language"):
|
if _personality_data.get("language"):
|
||||||
cfg.language = _persona_data["language"]
|
cfg.language = _personality_data["language"]
|
||||||
if _persona_data.get("mood"):
|
if _personality_data.get("mood"):
|
||||||
cfg.mood = _persona_data["mood"]
|
cfg.mood = _personality_data["mood"]
|
||||||
# Skill wajib dari persona.yaml
|
# Skill wajib dari personality.yaml
|
||||||
_skill_from_yaml = _persona_data.get("skill") or _persona_data.get("mode")
|
_skill_from_yaml = _personality_data.get("skill") or _personality_data.get("mode")
|
||||||
if _skill_from_yaml:
|
if _skill_from_yaml:
|
||||||
selected_skill = _skill_from_yaml.strip().lower()
|
selected_skill = _skill_from_yaml.strip().lower()
|
||||||
else:
|
else:
|
||||||
selected_skill = ""
|
selected_skill = ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}", flush=True)
|
print(f"[personality] Warning: gagal load personality.yaml untuk '{character_name}': {e}", flush=True)
|
||||||
|
|
||||||
# Resolve skills list
|
# Resolve skills list
|
||||||
# Priority: explicit skills param > persona.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill
|
# Priority: explicit skills param > personality.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill
|
||||||
if skills is not None:
|
if skills is not None:
|
||||||
skills_list = skills
|
skills_list = skills
|
||||||
elif selected_skill != SKILL:
|
elif selected_skill != SKILL:
|
||||||
# persona.yaml meng-override skill → pakai skill dari persona.yaml
|
# personality.yaml meng-override skill → pakai skill dari personality.yaml
|
||||||
skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst") else []
|
skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst") else []
|
||||||
else:
|
else:
|
||||||
skills_env = os.getenv("AGENT_SKILLS", default="").strip()
|
skills_env = os.getenv("AGENT_SKILLS", default="").strip()
|
||||||
@ -8,7 +8,7 @@ from datetime import datetime
|
|||||||
import config
|
import config
|
||||||
from services.session_manager import SessionManager
|
from services.session_manager import SessionManager
|
||||||
from services.agent_loop import run_agent_loop
|
from services.agent_loop import run_agent_loop
|
||||||
from scripts.persona import PERSONALITY
|
from scripts.personality import PERSONALITY
|
||||||
from tools.roleplayer import _name_mentioned
|
from tools.roleplayer import _name_mentioned
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from services.agent_loop import run_agent_loop
|
|||||||
|
|
||||||
import config
|
import config
|
||||||
from tools.roleplayer import should_respond
|
from tools.roleplayer import should_respond
|
||||||
from scripts.persona import PERSONALITY
|
from scripts.personality import PERSONALITY
|
||||||
|
|
||||||
# Anti-ban: delay constants for MUC rejoin behavior
|
# Anti-ban: delay constants for MUC rejoin behavior
|
||||||
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
|
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
|
||||||
|
|||||||
55
tui/agent.py
55
tui/agent.py
@ -87,18 +87,55 @@ def _agent_loop(app):
|
|||||||
log(app, "system", f" step {step + 1} \u2014 Thinking...")
|
log(app, "system", f" step {step + 1} \u2014 Thinking...")
|
||||||
app.scroll = 999999
|
app.scroll = 999999
|
||||||
|
|
||||||
response = app.llm.chat(app.messages, tools=app.TOOLS)
|
# Streaming response
|
||||||
|
stream_buffer = []
|
||||||
|
placeholder_marker = None
|
||||||
|
|
||||||
app.log.pop()
|
def on_stream_chunk(chunk):
|
||||||
|
nonlocal placeholder_marker
|
||||||
|
# Buat placeholder di chunk pertama saja
|
||||||
|
if placeholder_marker is None:
|
||||||
|
placeholder_marker = f"_stream_placeholder_{step}"
|
||||||
|
log(app, "ai", placeholder_marker)
|
||||||
|
stream_buffer.append(chunk)
|
||||||
|
current_text = ''.join(stream_buffer)
|
||||||
|
# Update placeholder secara real-time
|
||||||
|
for i in range(len(app.log) - 1, -1, -1):
|
||||||
|
# Cari placeholder berdasarkan content aslinya (atau apa yang sudah diupdate)
|
||||||
|
# Karena placeholder text berubah seiring streaming, kita harus teliti
|
||||||
|
if app.log[i].get('role') == 'ai' and (app.log[i].get('text') == placeholder_marker or (i > 0 and app.log[i-1].get('role') == 'system' and 'Thinking' in app.log[i-1].get('text', ''))):
|
||||||
|
app.log[i]['text'] = current_text
|
||||||
|
break
|
||||||
|
app.scroll = 999999
|
||||||
|
|
||||||
|
response = app.llm.chat(app.messages, tools=app.TOOLS, on_stream_chunk=on_stream_chunk)
|
||||||
|
|
||||||
|
# Hapus "Thinking..." log
|
||||||
|
for i in range(len(app.log) - 1, -1, -1):
|
||||||
|
if app.log[i].get('role') == 'system' and 'Thinking' in app.log[i].get('text', ''):
|
||||||
|
app.log.pop(i)
|
||||||
|
break
|
||||||
|
|
||||||
if response.warning:
|
if response.warning:
|
||||||
log(app, "system", f" {response.warning}")
|
log(app, "system", f" {response.warning}")
|
||||||
|
|
||||||
if response.tool_calls:
|
# Cek apakah ada tool_calls
|
||||||
|
has_tool_calls = bool(response.tool_calls)
|
||||||
|
|
||||||
|
if has_tool_calls:
|
||||||
|
# Hapus placeholder jika ada tool_calls (cari semua AI log yang mungkin placeholder)
|
||||||
|
if placeholder_marker is not None:
|
||||||
|
for i in range(len(app.log) - 1, -1, -1):
|
||||||
|
# Jika ini adalah log AI yang muncul tepat setelah "Thinking..." atau contains placeholder marker
|
||||||
|
if app.log[i].get('role') == 'ai':
|
||||||
|
text = app.log[i].get('text', '')
|
||||||
|
# Hapus jika text-nya adalah placeholder atau jika itu adalah entry AI yang baru saja kita buat untuk streaming
|
||||||
|
if placeholder_marker in text or text == "" or text == "...":
|
||||||
|
app.log.pop(i)
|
||||||
|
|
||||||
_add_msg(app, "assistant", response.content, tool_calls=response.tool_calls)
|
_add_msg(app, "assistant", response.content, tool_calls=response.tool_calls)
|
||||||
if response.content and response.content.strip():
|
|
||||||
log(app, "ai", response.content)
|
# Log tool_calls dengan label AI
|
||||||
app.scroll = 999999
|
|
||||||
for tc in response.tool_calls:
|
for tc in response.tool_calls:
|
||||||
tname = tc["function"]["name"]
|
tname = tc["function"]["name"]
|
||||||
targs = tc["function"]["arguments"]
|
targs = tc["function"]["arguments"]
|
||||||
@ -108,10 +145,14 @@ def _agent_loop(app):
|
|||||||
}))
|
}))
|
||||||
app.scroll = 999999
|
app.scroll = 999999
|
||||||
execute_tool(app, tc)
|
execute_tool(app, tc)
|
||||||
|
|
||||||
|
# Log content AI setelah tools (jika ada)
|
||||||
|
if response.content and response.content.strip():
|
||||||
|
log(app, "ai", response.content)
|
||||||
else:
|
else:
|
||||||
if response.content:
|
if response.content:
|
||||||
_add_msg(app, "assistant", response.content)
|
_add_msg(app, "assistant", response.content)
|
||||||
log(app, "ai", response.content)
|
# Placeholder sudah terupdate via streaming, jangan log lagi
|
||||||
log(app, "sep", "")
|
log(app, "sep", "")
|
||||||
ntro.end(stamp)
|
ntro.end(stamp)
|
||||||
app.agent_done.set()
|
app.agent_done.set()
|
||||||
|
|||||||
@ -39,7 +39,13 @@ def handle_key(app, stdscr, key):
|
|||||||
processing = app.processing
|
processing = app.processing
|
||||||
|
|
||||||
# -- Always allowed (even during processing) --
|
# -- Always allowed (even during processing) --
|
||||||
if key == 3: # Ctrl+C → exit
|
if key == 3: # Ctrl+C → cancel stream jika processing, exit jika tidak
|
||||||
|
if processing:
|
||||||
|
# Cancel stream yang sedang berjalan
|
||||||
|
app.llm.cancel_requested = True
|
||||||
|
log(app, "system", " Stream cancelled by user")
|
||||||
|
app.scroll = 999999
|
||||||
|
else:
|
||||||
app.running = False
|
app.running = False
|
||||||
elif key == curses.KEY_PPAGE:
|
elif key == curses.KEY_PPAGE:
|
||||||
app.scroll = max(0, app.scroll - (app.h - 10) // 2)
|
app.scroll = max(0, app.scroll - (app.h - 10) // 2)
|
||||||
|
|||||||
@ -105,6 +105,23 @@ def draw_chat(app, stdscr):
|
|||||||
_add_row([(color, " " * indent + chunk, bold)])
|
_add_row([(color, " " * indent + chunk, bold)])
|
||||||
start += available
|
start += available
|
||||||
|
|
||||||
|
def _wrap_text_simple(text, indent=0, color=C_INPUT, bold=False):
|
||||||
|
"""Wrap text yang panjang tanpa break di dalam kata, dipakai untuk tool arguments."""
|
||||||
|
available = w - indent - 1
|
||||||
|
if available <= 0:
|
||||||
|
return
|
||||||
|
lines = text.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
_add_row([(color, " " * indent, bold)])
|
||||||
|
continue
|
||||||
|
# Jika line lebih panjang dari available, pecah
|
||||||
|
start = 0
|
||||||
|
while start < len(line):
|
||||||
|
chunk = line[start:start + available]
|
||||||
|
_add_row([(color, " " * indent + chunk, bold)])
|
||||||
|
start += available
|
||||||
|
|
||||||
for idx, item in enumerate(app.log):
|
for idx, item in enumerate(app.log):
|
||||||
role, text = item["role"], item["text"]
|
role, text = item["role"], item["text"]
|
||||||
if role == "sep":
|
if role == "sep":
|
||||||
@ -179,8 +196,8 @@ def draw_chat(app, stdscr):
|
|||||||
(C_TOOL_CALL, tname),
|
(C_TOOL_CALL, tname),
|
||||||
(C_AI, f" ({item['time']}) "),
|
(C_AI, f" ({item['time']}) "),
|
||||||
])
|
])
|
||||||
for aline in args_str.split("\n"):
|
# Wrap arguments sesuai lebar terminal
|
||||||
_add_row([(C_INPUT, " " + aline)])
|
_wrap_text_simple(args_str, indent=1, color=C_INPUT, bold=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
_add_row([
|
_add_row([
|
||||||
(C_AI, " Hendrik "),
|
(C_AI, " Hendrik "),
|
||||||
@ -344,6 +361,9 @@ def draw_status(app, stdscr):
|
|||||||
session_tag = f"[{sname}] "
|
session_tag = f"[{sname}] "
|
||||||
|
|
||||||
mode = " PROCESSING " if app.processing else " READY "
|
mode = " PROCESSING " if app.processing else " READY "
|
||||||
|
if app.processing:
|
||||||
|
hints = " ^N:new ^O:open ^R:rename ^E:model ^W:ws ^C:cancel "
|
||||||
|
else:
|
||||||
hints = " ^N:new ^O:open ^R:rename ^D:send ^E:model ^W:ws ^C:exit "
|
hints = " ^N:new ^O:open ^R:rename ^D:send ^E:model ^W:ws ^C:exit "
|
||||||
max_left = w - len(mode) - len(hints) - 4
|
max_left = w - len(mode) - len(hints) - 4
|
||||||
left = session_tag + ws
|
left = session_tag + ws
|
||||||
@ -368,7 +388,8 @@ def draw_status(app, stdscr):
|
|||||||
(" ^D:send", not app.processing),
|
(" ^D:send", not app.processing),
|
||||||
(" ^E:model", True),
|
(" ^E:model", True),
|
||||||
(" ^W:ws", True),
|
(" ^W:ws", True),
|
||||||
(" ^C:exit", True),
|
(" ^C:cancel", app.processing),
|
||||||
|
(" ^C:exit", not app.processing),
|
||||||
]
|
]
|
||||||
for text, enabled in hints_parts:
|
for text, enabled in hints_parts:
|
||||||
attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD if enabled else curses.color_pair(C_HINT_DISABLED)
|
attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD if enabled else curses.color_pair(C_HINT_DISABLED)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user