hendrik/tui/agent.py

130 lines
4.1 KiB
Python

# agent.py — Agent loop dan tool execution.
# submit() adalah entry point: membaca input buffer, mengirim ke LLM,
# memproses tool calls, dan menampilkan hasil di log.
import json
from datetime import datetime
from .render import draw
from scripts import ntro
def log(app, role, text):
# Simpan item ke app.log untuk di-render oleh render.py
app.log.append({
"role": role,
"text": text,
"time": datetime.now().strftime("%H:%M"),
})
def submit(app, stdscr):
# Kirim query dari input buffer ke LLM.
# Loop sampai LLM mengembalikan final answer (tanpa tool_calls)
# atau mencapai max_iterations.
query = "\n".join(app.input_buffer).strip()
if not query:
return
log(app, "user", query)
# Reset input buffer
app.input_buffer = [""]
app.input_line = 0
app.input_col = 0
app.scroll = 999999 # scroll ke paling bawah
app.processing = True
draw(app, stdscr)
stdscr.refresh()
app.messages.append({"role": "user", "content": query})
stamp = ntro.start()
for step in range(app.agent_max_iterations):
stamp_step = ntro.start()
log(app, "system", f" step {step + 1} \u2014 LLM...")
app.scroll = 999999
draw(app, stdscr)
stdscr.refresh()
response = app.llm.chat(app.messages, tools=app.TOOLS)
# Hapus "step N — LLM..." log, ganti dengan hasil aktual
app.log.pop()
if response.tool_calls:
# LLM meminta menjalankan tool(s)
amsg = {
"role": "assistant",
"content": response.content,
"tool_calls": response.tool_calls,
}
app.messages.append(amsg)
if response.content and response.content.strip():
log(app, "ai", response.content)
app.scroll = 999999
draw(app, stdscr)
stdscr.refresh()
for tc in response.tool_calls:
tname = tc["function"]["name"]
log(app, "system", f" \u2192 {tname}")
app.scroll = 999999
draw(app, stdscr)
stdscr.refresh()
execute_tool(app, tc)
else:
# Final answer — tidak ada tool_calls
if response.content:
app.messages.append({
"role": "assistant",
"content": response.content,
})
log(app, "ai", response.content)
log(app, "sep", "")
app.processing = False
app.scroll = 999999
draw(app, stdscr)
stdscr.refresh()
ntro.end(stamp)
return
ntro.end(stamp_step)
# Timeout — max iterations tercapai tanpa final answer
log(app, "error", "Max iterations reached without final answer.")
app.messages.append({"role": "assistant",
"content": "Max iterations reached without final answer."})
app.processing = False
ntro.end(stamp)
def execute_tool(app, tool_call):
# Dispatch tool_call ke handler yang terdaftar di TOOL_HANDLERS.
# search_code dan git_operation butuh penanganan argumen khusus.
tname = tool_call["function"]["name"]
targs = json.loads(tool_call["function"]["arguments"])
handler = app.TOOL_HANDLERS.get(tname)
if not handler:
result = f"Tool {tname} not found"
else:
try:
if tname == "search_code":
result = handler(
pattern=targs["pattern"],
search_type=targs["search_type"],
path=targs.get("path", "."),
)
elif tname == "git_operation":
result = handler(args=targs["args"])
else:
result = handler(**targs)
except Exception as e:
result = f"Error executing tool: {str(e)}"
# Hasil tool disimpan ke messages agar bisa dikirim balik ke LLM
app.messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(result),
})