diff --git a/config.py b/config.py
index eabd8a0..287ffec 100644
--- a/config.py
+++ b/config.py
@@ -5,7 +5,7 @@ load_dotenv()
# LLM Configuration
LLM_BASE_URL = os.getenv("LLM_BASE_URL", default="http://localhost:11434/v1")
-LLM_MODEL = os.getenv("LLM_MODEL", default="deepseek-r1:8b")
+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 Configuration
diff --git a/hendrik.py b/hendrik.py
index 7b6f14a..ae7eddc 100644
--- a/hendrik.py
+++ b/hendrik.py
@@ -1,4 +1,4 @@
-import os, sys, json
+import os, sys, json, tty, termios
import config
from llm_client import LLMClient
from tools import coder
@@ -13,25 +13,79 @@ tools_definition = [
gadget.tools_mapping( coder.schema_git_operation, coder.git_operation ),
]
-TOOLS = [t["schema"] for t in tools_definition] # Schemas
-TOOL_HANDLERS = {t["name"]: t["handler"] for t in tools_definition} # Map
+TOOLS = gadget.tool_schemas(tools_definition)
+TOOL_HANDLERS = gadget.tool_handlers(tools_definition)
-SYSTEM_PROMPT = """You are a coding agent that assists with software engineering tasks. You have access to the following tools:
-1. read_file: Read file contents with line numbers
-2. write_file: Write content to a file (overwrites existing)
-3. edit_file: Replace text in a file
-4. run_bash: Execute bash commands
-5. search_code: Search for files (glob) or file contents (regex)
-6. git_operation: Run git commands
+def interactive_input():
+ fd = sys.stdin.fileno()
+ old = termios.tcgetattr(fd)
-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."""
+ print()
+ print("\u2500" * 50)
+ print("Hendrik AI Agent - Interactive Mode")
+ print("\u2500" * 50)
+ print(f"Workspace: {os.getcwd()}")
+ print("\u2500" * 50)
+ print("[Ctrl+W] Change workspace | :workspace
| [Ctrl+D] Submit")
+ print("\u2500" * 50)
-def agent_loop(user_query, llm_client):
- messages = [
- {"role": "system" , "content": SYSTEM_PROMPT },
- {"role": "user" , "content": user_query }
- ]
+ buffer = bytearray()
+ try:
+ tty.setraw(fd)
+ while True:
+ ch = os.read(fd, 1)
+ if ch == b'\x03': # Ctrl+C → exit
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
+ print("\r\nExiting.")
+ sys.exit(0)
+ elif ch == b'\x04': # Ctrl+D → submit
+ break
+ elif ch == b'\x17': # Ctrl+W → change workspace
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
+ print("\r\n", end="")
+ ws = input("Workspace directory: ").strip()
+ if ws:
+ resolved = os.path.abspath(ws)
+ if not os.path.isdir(resolved):
+ print(f"Error: '{resolved}' is not a valid directory")
+ else:
+ os.chdir(resolved)
+ print(f"\u2192 Workspace changed to {os.getcwd()}")
+ return interactive_input()
+ elif ch in (b'\r', b'\n'): # Enter
+ buffer.extend(b'\n')
+ sys.stdout.buffer.write(b'\r\n')
+ sys.stdout.flush()
+ elif ch == b'\x7f': # Backspace
+ if buffer:
+ buffer.pop()
+ sys.stdout.buffer.write(b'\b \b')
+ sys.stdout.flush()
+ elif ch >= b' ': # Printable characters
+ buffer.extend(ch)
+ sys.stdout.buffer.write(ch)
+ sys.stdout.flush()
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
+
+ full_query = buffer.decode('utf-8', errors='replace').strip()
+
+ if full_query.startswith(':workspace '):
+ ws = full_query[11:].strip()
+ resolved = os.path.abspath(ws)
+ if not os.path.isdir(resolved):
+ print(f"Error: '{resolved}' is not a valid directory")
+ return interactive_input()
+ os.chdir(resolved)
+ print(f"\u2192 Workspace changed to {os.getcwd()}")
+ return interactive_input()
+
+ return full_query
+
+
+def agent_loop(user_query, messages, llm_client):
+ messages.append({"role": "user", "content": user_query})
for _ in range(config.AGENT_MAX_ITERATIONS):
response = llm_client.chat(messages, tools=TOOLS)
if response.tool_calls:
@@ -48,6 +102,8 @@ def agent_loop(user_query, llm_client):
if not handler:
result = f"Tool {tool_name} not found"
else:
+ args_display = ", ".join(f"{k}={v!r}" for k, v in tool_args.items())
+ print(f" \u2192 {tool_name}({args_display})")
try:
if tool_name == "search_code":
result = handler(
@@ -67,23 +123,67 @@ def agent_loop(user_query, llm_client):
"content": str(result)
})
else:
- return response.content
- return "Max iterations reached without final answer."
+ messages.append({"role": "assistant", "content": response.content})
+ return response.content, messages
+ msg = "Max iterations reached without final answer."
+ messages.append({"role": "assistant", "content": msg})
+ return msg, messages
+
def main():
- llm_client = LLMClient(base_url=config.LLM_BASE_URL, model=config.LLM_MODEL, api_key=config.LLM_API_KEY)
- if len(sys.argv) > 1:
- user_query = " ".join(sys.argv[1:])
- else:
- print("Enter your query (Ctrl+D to submit):")
- user_query = sys.stdin.read().strip()
- if not user_query:
- print("No query provided.")
+ workspace = None
+ query_parts = []
+ i = 1
+ while i < len(sys.argv):
+ if sys.argv[i] in ('-w', '--workspace') and i + 1 < len(sys.argv):
+ workspace = sys.argv[i + 1]
+ i += 2
+ else:
+ query_parts.append(sys.argv[i])
+ i += 1
+
+ if workspace:
+ resolved = os.path.abspath(workspace)
+ if not os.path.isdir(resolved):
+ print(f"Error: '{resolved}' is not a valid directory")
+ sys.exit(1)
+ os.chdir(resolved)
+
+ llm_client = LLMClient(
+ base_url=config.LLM_BASE_URL,
+ model=config.LLM_MODEL,
+ api_key=config.LLM_API_KEY
+ )
+
+ messages = None
+
+ if query_parts:
+ user_query = ' '.join(query_parts)
+ if not user_query:
+ print("No query provided.")
+ return
+ messages = [{"role": "system", "content": gadget.build_system_prompt(tools_definition)}]
+ print("Thinking...")
+ final_answer, messages = agent_loop(user_query, messages, llm_client)
+ print("\nFinal Answer:")
+ print(final_answer)
return
- print("Thinking...")
- final_answer = agent_loop(user_query, llm_client)
- print("\nFinal Answer:")
- print(final_answer)
+
+ while True:
+ user_query = interactive_input()
+ if not user_query:
+ break
+ if user_query.lower() in ('/exit', '/quit'):
+ break
+
+ if messages is None:
+ messages = [{"role": "system", "content": gadget.build_system_prompt(tools_definition)}]
+
+ print("Thinking...")
+ final_answer, messages = agent_loop(user_query, messages, llm_client)
+ print("\nFinal Answer:")
+ print(final_answer)
+
if __name__ == "__main__":
main()
diff --git a/sample-prompt b/sample-prompt
deleted file mode 100644
index 2964cc3..0000000
--- a/sample-prompt
+++ /dev/null
@@ -1,6 +0,0 @@
-Buatkan aku permainan menebak angka dadu dengan python dengan nama `dice.py`.
-Pemain dapat melihat angka dadu di awal permainan.
-Pemain akan menebak apakah angka selanjutnya lebih rendah atau lebih tinggi.
-Jika tebakan pemain benar maka score bertambah 10. Jika salah maka score di reset jadi 0.
-Tampilkan info `Score` dan `Dice`. Input hanya ada `low`, `high`, dan `quit`.
-Permainan akan terus berlanjut sampai pemain keluar dari permainan.
\ No newline at end of file
diff --git a/scripts/gadget.py b/scripts/gadget.py
index fd70fde..53b6fef 100644
--- a/scripts/gadget.py
+++ b/scripts/gadget.py
@@ -1,4 +1,37 @@
+import os
+
+
def tools_mapping(schema, handler, name=None):
tool_name = name or schema["function"]["name"]
return {"name": tool_name, "schema": schema, "handler": handler}
+
+def tool_schemas(tools_definition):
+ return [t["schema"] for t in tools_definition]
+
+
+def tool_handlers(tools_definition):
+ return {t["name"]: t["handler"] for t in tools_definition}
+
+
+def build_system_prompt(tools_definition):
+ lines = [
+ "You are a coding agent that assists with software engineering tasks. "
+ "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.extend([
+ "",
+ "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.",
+ "",
+ f"Your workspace directory is: {os.getcwd()}. "
+ "All file operations are relative to this directory."
+ ])
+ return "\n".join(lines)
+