Skip to content

Commit 0155273

Browse files
committed
Add interactive TUI for memex and dagit
Textual-based chat interface with OpenAI function calling. Combines memex knowledge graph tools (search, traverse, create) with dagit social network tools (post, read, reply, verify).
1 parent 1c4cff8 commit 0155273

7 files changed

Lines changed: 847 additions & 0 deletions

File tree

tui/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
memex-tui-venv/
2+
__pycache__/
3+
*.egg-info/

tui/memex_tui/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Memex TUI - Interactive control plane for memex and dagit."""
2+
3+
from .app import MemexApp
4+
5+
6+
def main():
7+
"""Entry point for memex-tui command."""
8+
app = MemexApp()
9+
app.run()
10+
11+
12+
__all__ = ["main", "MemexApp"]

tui/memex_tui/app.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Memex TUI Application."""
2+
3+
from textual.app import App, ComposeResult
4+
from textual.widgets import Input, Footer, Header
5+
from textual.containers import Container
6+
from textual.binding import Binding
7+
8+
from .chat import ChatPanel, ChatEngine
9+
10+
11+
class MemexApp(App):
12+
"""Interactive TUI for memex and dagit."""
13+
14+
CSS = """
15+
Screen {
16+
layout: grid;
17+
grid-size: 1;
18+
grid-rows: 1fr auto;
19+
}
20+
21+
#chat-container {
22+
height: 100%;
23+
border: solid $primary;
24+
padding: 1;
25+
}
26+
27+
#chat-log {
28+
height: 100%;
29+
scrollbar-gutter: stable;
30+
}
31+
32+
#input-container {
33+
height: auto;
34+
padding: 1;
35+
}
36+
37+
#input {
38+
dock: bottom;
39+
}
40+
41+
Footer {
42+
height: auto;
43+
}
44+
"""
45+
46+
BINDINGS = [
47+
Binding("ctrl+c", "quit", "Quit"),
48+
Binding("ctrl+l", "clear", "Clear"),
49+
Binding("escape", "focus_input", "Focus Input", show=False),
50+
]
51+
52+
TITLE = "Memex"
53+
54+
def __init__(self):
55+
super().__init__()
56+
self.chat_engine = ChatEngine()
57+
self._current_response = ""
58+
59+
def compose(self) -> ComposeResult:
60+
"""Create the UI layout."""
61+
yield Header()
62+
with Container(id="chat-container"):
63+
yield ChatPanel(id="chat-log")
64+
with Container(id="input-container"):
65+
yield Input(placeholder="Ask anything... (Ctrl+C to quit)", id="input")
66+
yield Footer()
67+
68+
def on_mount(self) -> None:
69+
"""Focus input on start."""
70+
self.query_one("#input", Input).focus()
71+
chat = self.query_one("#chat-log", ChatPanel)
72+
chat.add_system_message(
73+
"Welcome to Memex TUI. Ask questions about your knowledge graph or dagit network."
74+
)
75+
chat.add_system_message('Type "help" for commands, Ctrl+C to quit.')
76+
77+
async def on_input_submitted(self, event: Input.Submitted) -> None:
78+
"""Handle user input."""
79+
user_input = event.value.strip()
80+
if not user_input:
81+
return
82+
83+
# Clear input
84+
input_widget = self.query_one("#input", Input)
85+
input_widget.value = ""
86+
87+
chat = self.query_one("#chat-log", ChatPanel)
88+
89+
# Handle special commands
90+
if user_input.lower() in ("exit", "quit"):
91+
self.exit()
92+
return
93+
94+
if user_input.lower() == "clear":
95+
self.action_clear()
96+
return
97+
98+
if user_input.lower() == "help":
99+
self._show_help(chat)
100+
return
101+
102+
# Show user message
103+
chat.add_user_message(user_input)
104+
105+
# Start assistant response
106+
chat.start_assistant_response()
107+
self._current_response = ""
108+
109+
# Stream response
110+
async def on_text(text: str) -> None:
111+
self._current_response += text
112+
chat.add_assistant_text(text)
113+
114+
async def on_tool(tool_name: str) -> None:
115+
chat.add_tool_indicator(tool_name)
116+
117+
try:
118+
await self.chat_engine.send(user_input, on_text, on_tool)
119+
except Exception as e:
120+
chat.add_error(str(e))
121+
122+
# Ensure we end on a new line
123+
if self._current_response and not self._current_response.endswith("\n"):
124+
chat.write("")
125+
126+
def _show_help(self, chat: ChatPanel) -> None:
127+
"""Show help message."""
128+
chat.add_system_message("Commands:")
129+
chat.add_system_message(" help - Show this help")
130+
chat.add_system_message(" clear - Clear chat history")
131+
chat.add_system_message(" exit - Quit the application")
132+
chat.add_system_message("")
133+
chat.add_system_message("Examples:")
134+
chat.add_system_message(' "search for notes about topic"')
135+
chat.add_system_message(' "what\'s my dagit identity"')
136+
chat.add_system_message(' "save this as a note: <your content>"')
137+
chat.add_system_message(' "post to dagit: <your message>"')
138+
139+
def action_clear(self) -> None:
140+
"""Clear chat history."""
141+
self.chat_engine.clear()
142+
chat = self.query_one("#chat-log", ChatPanel)
143+
chat.clear()
144+
chat.add_system_message("Chat cleared.")
145+
146+
def action_focus_input(self) -> None:
147+
"""Focus the input field."""
148+
self.query_one("#input", Input).focus()
149+
150+
def action_quit(self) -> None:
151+
"""Quit the application."""
152+
self.exit()

tui/memex_tui/chat.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Chat panel widget for Memex TUI."""
2+
3+
import json
4+
from typing import Callable, Awaitable
5+
6+
from textual.widgets import RichLog
7+
from rich.markdown import Markdown
8+
from rich.text import Text
9+
10+
from .provider import ChatProvider, Chunk, ToolCall
11+
from .tools import get_all_tools, execute_tool
12+
13+
14+
SYSTEM_PROMPT = """You are Memex, an intelligent assistant with access to a knowledge graph (memex) and a decentralized social network (dagit).
15+
16+
Available capabilities:
17+
- Search and query the knowledge graph (memex_search, memex_get_node, memex_traverse, etc.)
18+
- Create notes and save information (memex_create_node)
19+
- Post to the dagit social network (dagit_post)
20+
- Read posts from dagit (dagit_read)
21+
- Check your identity (dagit_whoami)
22+
23+
When users ask questions:
24+
1. Use the appropriate tools to find information
25+
2. Synthesize the results into a helpful response
26+
3. If saving information, confirm what was saved
27+
28+
Be concise but helpful. Use the tools proactively when they would help answer the user's question."""
29+
30+
31+
class ChatEngine:
32+
"""Manages chat state and model interactions."""
33+
34+
def __init__(self):
35+
self.provider = ChatProvider()
36+
self.messages: list[dict] = []
37+
self.tools = get_all_tools()
38+
39+
async def send(
40+
self,
41+
user_input: str,
42+
on_text: Callable[[str], Awaitable[None]],
43+
on_tool: Callable[[str], Awaitable[None]],
44+
) -> None:
45+
"""Send a message and stream the response.
46+
47+
Args:
48+
user_input: User's message
49+
on_text: Callback for text chunks
50+
on_tool: Callback for tool call notifications
51+
"""
52+
self.messages.append({"role": "user", "content": user_input})
53+
54+
max_turns = 10
55+
for _ in range(max_turns):
56+
text_buffer = ""
57+
tool_calls: list[ToolCall] = []
58+
59+
for chunk in self.provider.stream(
60+
SYSTEM_PROMPT, self.messages, self.tools
61+
):
62+
if chunk.type == "text":
63+
await on_text(chunk.text)
64+
text_buffer += chunk.text
65+
66+
elif chunk.type == "tool_call":
67+
tool_calls.append(chunk.tool_call)
68+
await on_tool(f"[{chunk.tool_call.name}]")
69+
70+
elif chunk.type == "error":
71+
await on_text(f"\nError: {chunk.error}")
72+
return
73+
74+
# If there were tool calls, execute them and continue
75+
if tool_calls:
76+
# Add assistant message with tool calls
77+
self.messages.append(
78+
{
79+
"role": "assistant",
80+
"content": text_buffer or None,
81+
"tool_calls": [
82+
{
83+
"id": tc.id,
84+
"type": "function",
85+
"function": {
86+
"name": tc.name,
87+
"arguments": json.dumps(tc.arguments),
88+
},
89+
}
90+
for tc in tool_calls
91+
],
92+
}
93+
)
94+
95+
# Execute tools and add results
96+
for tc in tool_calls:
97+
result = execute_tool(tc.name, tc.arguments)
98+
self.messages.append(
99+
{
100+
"role": "tool",
101+
"tool_call_id": tc.id,
102+
"content": result,
103+
}
104+
)
105+
continue
106+
107+
# No tool calls - conversation turn complete
108+
if text_buffer:
109+
self.messages.append({"role": "assistant", "content": text_buffer})
110+
return
111+
112+
def clear(self) -> None:
113+
"""Clear conversation history."""
114+
self.messages.clear()
115+
116+
117+
class ChatPanel(RichLog):
118+
"""Chat display panel with rich formatting."""
119+
120+
def __init__(self, **kwargs):
121+
super().__init__(markup=True, wrap=True, **kwargs)
122+
123+
def add_user_message(self, text: str) -> None:
124+
"""Add a user message to the display."""
125+
self.write(Text.from_markup(f"[bold cyan]You:[/bold cyan] {text}"))
126+
127+
def add_assistant_text(self, text: str) -> None:
128+
"""Add assistant text (streaming)."""
129+
# For streaming, we append to the current line
130+
self.write(text, scroll_end=True)
131+
132+
def start_assistant_response(self) -> None:
133+
"""Start a new assistant response line."""
134+
self.write(Text.from_markup("[bold green]Memex:[/bold green] "), scroll_end=True)
135+
136+
def add_tool_indicator(self, tool_name: str) -> None:
137+
"""Show a tool being called."""
138+
self.write(Text.from_markup(f"[dim]{tool_name}[/dim]"))
139+
140+
def add_error(self, error: str) -> None:
141+
"""Display an error message."""
142+
self.write(Text.from_markup(f"[bold red]Error:[/bold red] {error}"))
143+
144+
def add_system_message(self, text: str) -> None:
145+
"""Display a system message."""
146+
self.write(Text.from_markup(f"[dim italic]{text}[/dim italic]"))

0 commit comments

Comments
 (0)