diff --git a/scripts/ntro.py b/scripts/ntro.py new file mode 100644 index 0000000..05daa6b --- /dev/null +++ b/scripts/ntro.py @@ -0,0 +1,12 @@ +import time + +def start(): + return {"_start": time.perf_counter_ns()} + +def end(stamp): + elapsed_ns = time.perf_counter_ns() - stamp["_start"] + elapsed_ms = elapsed_ns / 1_000_000 + print(f"[ntropy] {elapsed_ms:.2f}ms") + stamp["_start"] = time.perf_counter_ns() + return elapsed_ms + diff --git a/tui/agent.py b/tui/agent.py new file mode 100644 index 0000000..52af6b8 --- /dev/null +++ b/tui/agent.py @@ -0,0 +1,129 @@ +# 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), + })