Skip to content

Commit c12b2ef

Browse files
committed
Implement simple AI chat
1 parent 3d7784c commit c12b2ef

5 files changed

Lines changed: 339 additions & 34 deletions

File tree

templates/pages/ai.html

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
<button id="clearChat" class="clear__button">Clear Chat</button>
1515
</div>
1616
<div class="chat__log" id="chat"></div>
17+
<div class="context__usage" id="contextUsage" style="display: none;">
18+
<span>Context: </span><span id="contextPercent">0%</span>
19+
</div>
1720
<form class="chat__form" id="form">
1821
<input class="form__input" id="input" placeholder="Ask anything about networks" autocomplete="off" />
1922
<button class="form__send" type="submit">Send</button>
@@ -64,6 +67,9 @@
6467
background: #fff3e0;
6568
padding: 8px;
6669
margin: 5px 0;
70+
overflow: hidden;
71+
text-overflow: ellipsis;
72+
white-space: nowrap;
6773
border-radius: 4px;
6874
font-family: monospace;
6975
font-size: 0.9em;
@@ -76,6 +82,52 @@
7682
margin: 5px 0;
7783
border-radius: 4px;
7884
}
85+
86+
.log__assistant {
87+
background: #f8f9fa;
88+
padding: 8px;
89+
margin: 5px 0;
90+
border-radius: 4px;
91+
border-left: 3px solid #007bff;
92+
}
93+
94+
.log__user {
95+
background: #e8f5e8;
96+
padding: 8px;
97+
margin: 5px 0;
98+
border-radius: 4px;
99+
border-left: 3px solid #28a745;
100+
}
101+
102+
.context__usage {
103+
text-align: right;
104+
padding: 5px 10px;
105+
font-size: 0.85em;
106+
color: #666;
107+
background: #f8f9fa;
108+
border-top: 1px solid #dee2e6;
109+
}
110+
111+
.chat__form {
112+
display: flex;
113+
gap: 10px;
114+
padding: 10px;
115+
}
116+
117+
.form__input {
118+
flex: 1;
119+
padding: 8px;
120+
border: 1px solid #ddd;
121+
border-radius: 4px;
122+
}
123+
124+
.form__send {
125+
padding: 8px 16px;
126+
background: #007bff;
127+
color: white;
128+
border: none;
129+
border-radius: 4px;
130+
}
79131
</style>
80132

81133
<script>
@@ -84,17 +136,29 @@
84136
const input = document.getElementById('input')
85137
const modelSelect = document.getElementById('modelSelect')
86138
const clearButton = document.getElementById('clearChat')
139+
const contextUsage = document.getElementById('contextUsage')
140+
const contextPercent = document.getElementById('contextPercent')
87141

88142
let history = []
89143
let ws = null
144+
let currentModel = null
145+
146+
function updateTokenUsage(tokens, model) {
147+
currentModel = model
148+
contextUsage.style.display = 'block'
149+
let maxTokens = 5000
150+
151+
const percentage = Math.min((tokens / maxTokens) * 100, 100)
152+
contextPercent.textContent = `${Math.round(percentage)}%`
153+
}
90154

91155
async function initialize() {
92156
try {
93157
const response = await fetch('/api/v1/ai/models')
94158
const data = await response.json()
95159

96160
if (data.models) {
97-
modelSelect.innerHTML = '<option value="">Select a model (default: OpenRouter default)</option>'
161+
modelSelect.innerHTML = '<option value="">Auto</option>'
98162
data.models.forEach(model => {
99163
const option = document.createElement('option')
100164
option.value = model.id
@@ -132,7 +196,13 @@
132196

133197
function handle(data) {
134198
switch (data.type) {
199+
case 'token_usage':
200+
updateTokenUsage(data.tokens, data.model)
201+
break
135202
case 'tool_call':
203+
if (window.currentAssistantEl) {
204+
window.currentAssistantEl = null
205+
}
136206
append('thinking', `Calling ${data.name}...`)
137207
break
138208
case 'tool_result':
@@ -142,16 +212,20 @@
142212
if (!window.currentAssistantEl) {
143213
window.currentAssistantEl = document.createElement('div')
144214
window.currentAssistantEl.className = 'log__assistant'
145-
window.currentAssistantEl.textContent = ''
215+
window.currentAssistantEl.innerHTML = ''
146216
chat.appendChild(window.currentAssistantEl)
147217
chat.scrollTop = chat.scrollHeight
148218
}
149-
window.currentAssistantEl.textContent += data.delta
219+
// Convert newlines to <br> tags and escape HTML
220+
const escapedDelta = data.delta.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')
221+
window.currentAssistantEl.innerHTML += escapedDelta
150222
chat.scrollTop = chat.scrollHeight
151223
break
152224
case 'assistant_done':
153225
if (window.currentAssistantEl) {
154-
history.push({ role: 'assistant', content: window.currentAssistantEl.textContent })
226+
// Store the raw text content (without HTML) in history
227+
const rawContent = window.currentAssistantEl.textContent
228+
history.push({ role: 'assistant', content: rawContent })
155229
window.currentAssistantEl = null
156230
}
157231
break
@@ -187,6 +261,7 @@
187261
history = []
188262
chat.innerHTML = ''
189263
window.currentAssistantEl = null
264+
contextUsage.style.display = 'none'
190265
})
191266

192267
initialize()

templates/pages/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ <h1 class="container__heading">
4646
</div>
4747
</form>
4848
<div class="container__cards">
49-
<a class="cards__item">
49+
<a class="cards__item" href="/ai">
5050
<div class="item__title">AI</div>
5151
<div class="item__description">
5252
Supercharge your productivity with an easy-to-use interface
5353
</div>
5454
<i class="fa-sharp fa-light fa-sparkles"></i>
5555
</a>
56-
<a class="cards__item">
56+
<a class="cards__item" href="/telescope">
5757
<div class="item__title">Telescope</div>
5858
<div class="item__description">
5959
Query all our real-time data across the global routing table

utils/ai.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import List, Dict, Any, Generator
33
from utils.database import get_db, close_db
44
from flask import current_app as app
5+
from datetime import datetime, date
56
from config import Config
67
from openai import OpenAI
78
import json
@@ -29,20 +30,27 @@ def _build_tools_schema() -> List[Dict[str, Any]]:
2930
})
3031
return out_tools
3132

33+
class DateTimeEncoder(json.JSONEncoder):
34+
"""Custom JSON encoder that handles datetime objects."""
35+
def default(self, obj):
36+
if isinstance(obj, (datetime, date)):
37+
return obj.isoformat()
38+
return super().default(obj)
39+
3240
def _execute_tool(name: str, arguments: Dict[str, Any]) -> str:
3341
"""Execute MCP tool and return JSON result."""
3442
db = None
3543
try:
3644
db = get_db()
37-
rows = mcp_run_tool(name, arguments or {}, db)
38-
return json.dumps({'rows': rows})
45+
result = mcp_run_tool(name, arguments or {}, db)
46+
return json.dumps(result, cls=DateTimeEncoder)
3947
except Exception as e:
4048
app.logger.error('Tool execution failed: %s', str(e), exc_info=True)
41-
return json.dumps({'error': str(e)})
49+
return json.dumps({'error': str(e)}, cls=DateTimeEncoder)
4250
finally:
4351
close_db(db)
4452

45-
def chat(messages: List[Dict[str, str]], model: str = None) -> Generator[Dict[str, Any], None, None]:
53+
def chat(messages: List[Dict[str, str]], model: str = "openrouter/auto") -> Generator[Dict[str, Any], None, None]:
4654
"""
4755
Stream chat with OpenAI models using function calling.
4856
Let the LLM handle its own thinking process.
@@ -71,8 +79,9 @@ def chat(messages: List[Dict[str, str]], model: str = None) -> Generator[Dict[st
7179
tools=tools,
7280
tool_choice='auto',
7381
temperature=0.2,
74-
max_tokens=2000,
75-
stream=True
82+
max_tokens=5000,
83+
stream=True,
84+
stream_options={"include_usage": True},
7685
)
7786

7887
# Process streaming response
@@ -83,6 +92,10 @@ def chat(messages: List[Dict[str, str]], model: str = None) -> Generator[Dict[st
8392
for chunk in response:
8493
if not chunk.choices:
8594
continue
95+
96+
# Handle usage data from streaming response
97+
if hasattr(chunk, 'usage') and chunk.usage:
98+
yield {'type': 'token_usage', 'tokens': chunk.usage.total_tokens, 'model': model}
8699

87100
delta = chunk.choices[0].delta
88101

@@ -142,12 +155,13 @@ def chat(messages: List[Dict[str, str]], model: str = None) -> Generator[Dict[st
142155
yield {'type': 'tool_result', 'name': name, 'output': output}
143156

144157
# Add tool result to conversation
145-
conversation.append({
158+
tool_message = {
146159
'role': 'tool',
147160
'tool_call_id': tool_call['id'],
148161
'name': name,
149162
'content': output
150-
})
163+
}
164+
conversation.append(tool_message)
151165

152166
# Continue to next round for tool calling
153167
continue

0 commit comments

Comments
 (0)