Skip to content

Commit 3d7784c

Browse files
committed
Add MCP based AI Chat
1 parent 35f18df commit 3d7784c

11 files changed

Lines changed: 1080 additions & 49 deletions

File tree

app.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
from flask import Flask, render_template, request, redirect, url_for, abort, current_app as app
22
from utils.text import time_ago, format_text, clean_text, alphanumeric_text, clean_text, sanitize_text, date_text
33
from routes.asn import asn_blueprint, asn_api_v1_blueprint
4-
from utils.socket import sock, broadcaster
4+
from routes.ai import ai_blueprint, ai_api_v1_blueprint
5+
from utils.socket import sock, subscriber, chat
6+
from utils.database import get_db, close_db
7+
from utils.mcp import list_tools, run_tool
58
from utils.cache import cache, caching
69
from utils.limiter import limiter
710
from flask_compress import Compress
811
from flask_talisman import Talisman
912
from utils.seo import get_sitemap
1013
from datetime import timedelta
1114
from logging import StreamHandler
15+
from sqlalchemy import text
1216
from config import Config
1317
import logging
18+
import gevent
1419
import atexit
15-
import sass # type: ignore
20+
import json
21+
import sass
1622
import sys
1723
import re
18-
import gevent
24+
1925
from gevent import monkey
20-
monkey.patch_all()
26+
monkey.patch_all(ssl=False)
2127

2228
def compile_scss():
2329
scss_file = 'static/styles/main.scss'
@@ -162,23 +168,26 @@ def sitemap_index_xml():
162168

163169

164170
"""
165-
Subscriptions
171+
WebSocket
166172
"""
167173

168174
# Initialize socket
169-
broadcaster.init_app(app)
175+
subscriber.init_app(app)
176+
chat.init_app(app)
170177

171178
# Register socket shutdown
172-
atexit.register(lambda: broadcaster.shutdown(wait=False))
179+
atexit.register(lambda: subscriber.shutdown(wait=False))
180+
atexit.register(lambda: chat.shutdown(wait=False))
173181

174182
# Start socket
175-
broadcaster.start()
183+
subscriber.start()
184+
chat.start()
176185

177186
@sock.route('/ws/v1/subscribe/<resource>')
178187
def ws_v1_subscribe(ws, resource):
179188
try:
180189
app.logger.info(f"WebSocket connected for {resource}")
181-
broadcaster.register(ws, resource=resource)
190+
subscriber.register(ws, resource=resource)
182191

183192
while True:
184193
try:
@@ -197,14 +206,41 @@ def ws_v1_subscribe(ws, resource):
197206
app.logger.error(f"WebSocket connection error for {resource}: {str(e)}", exc_info=True)
198207
finally:
199208
app.logger.info(f"WebSocket disconnected for {resource}")
200-
broadcaster.unregister(ws, resource)
209+
subscriber.unregister(ws, resource)
210+
211+
@sock.route('/ws/v1/chat')
212+
def ws_v1_chat(ws):
213+
try:
214+
app.logger.info(f"WebSocket connected for chat")
215+
chat.register(ws)
216+
217+
while True:
218+
# Receive message from client
219+
message = ws.receive()
220+
if not message:
221+
break
222+
223+
try:
224+
data = json.loads(message)
225+
chat.handle(ws, data)
226+
227+
except json.JSONDecodeError:
228+
ws.send(json.dumps({'type': 'error', 'message': 'Invalid JSON format'}))
229+
except Exception as e:
230+
app.logger.error("WebSocket chat error: %s", str(e), exc_info=True)
231+
ws.send(json.dumps({'type': 'error', 'message': str(e)}))
232+
233+
except Exception as e:
234+
app.logger.error("WebSocket connection error: %s", str(e), exc_info=True)
235+
finally:
236+
app.logger.info(f"WebSocket disconnected for chat")
237+
chat.unregister(ws)
201238

202239

203240
"""
204-
Basic
241+
Base
205242
"""
206243

207-
208244
@app.route('/')
209245
@caching(timeout=86400) # 24 hours
210246
def index():
@@ -221,6 +257,8 @@ def index():
221257
# Register Blueprints
222258
app.register_blueprint(asn_blueprint, url_prefix='/as')
223259
app.register_blueprint(asn_api_v1_blueprint, url_prefix='/api/v1/as')
260+
app.register_blueprint(ai_blueprint, url_prefix='/ai')
261+
app.register_blueprint(ai_api_v1_blueprint, url_prefix='/api/v1/ai')
224262

225263
return app
226264

config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class Config():
1919
# Postmark
2020
POSTMARK_API_KEY = os.getenv('POSTMARK_API_KEY')
2121

22+
# OpenRouter
23+
OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
24+
2225
# Environment
2326
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development').lower()
2427
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG').upper()
@@ -31,12 +34,13 @@ def validate():
3134
'FLASK_HOST': Config.FLASK_HOST,
3235
'KAFKA_FQDN': Config.KAFKA_FQDN,
3336
'KAFKA_JMX_FQDN': Config.KAFKA_JMX_FQDN,
34-
'POSTMARK_API_KEY': Config.POSTMARK_API_KEY,
3537
'POSTGRES_HOST': Config.POSTGRES_HOST,
3638
'POSTGRES_PORT': Config.POSTGRES_PORT,
3739
'POSTGRES_DB': Config.POSTGRES_DB,
3840
'POSTGRES_USER': Config.POSTGRES_USER,
3941
'POSTGRES_PASSWORD': Config.POSTGRES_PASSWORD,
42+
'POSTMARK_API_KEY': Config.POSTMARK_API_KEY,
43+
'OPENROUTER_API_KEY': Config.OPENROUTER_API_KEY,
4044
}
4145
for key, val in required.items():
4246
if not val:

manage.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,21 @@ def run(workers="1", host="localhost", port=8080, reload=False):
1212
"""
1313
Runs the application.
1414
"""
15-
# Validate the configuration
15+
# Validate.
1616
Config.validate()
1717

1818
if reload:
19-
# Get the app instance
2019
from app import create_app
2120
app = create_app()
22-
# Use the built-in Flask development server which works with flask-sock
23-
app.run(host=host, port=port, debug=True, extra_files=['templates/**/*.html', 'static/**/*.scss'])
21+
app.run(
22+
host=host,
23+
port=port,
24+
debug=True,
25+
extra_files=[
26+
'templates/**/*.html',
27+
'static/**/*.scss'
28+
]
29+
)
2430
else:
2531
subprocess.run([
2632
"gunicorn",

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ confluent-kafka==2.5.3
2020
rocksdb-py==0.0.5
2121
scapy==2.6.1
2222
SQLAlchemy==2.0.36
23-
psycopg2-binary==2.9.10
23+
psycopg2-binary==2.9.10
24+
openai==2.1.0

routes/ai.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from flask import Blueprint, render_template, jsonify, current_app as app
2+
from utils.ai import client
3+
4+
# Create Blueprints
5+
ai_blueprint = Blueprint('ai', __name__)
6+
ai_api_v1_blueprint = Blueprint('ai_api_v1', __name__)
7+
8+
@ai_blueprint.route('/')
9+
def page():
10+
return render_template('pages/ai.html')
11+
12+
@ai_api_v1_blueprint.route('/models', methods=['GET'])
13+
def api_v1_ai_models():
14+
try:
15+
models_response = client.models.list()
16+
models = []
17+
18+
for model in models_response.data:
19+
if hasattr(model, 'id'):
20+
models.append({
21+
'id': model.id,
22+
'name': getattr(model, 'name', model.id),
23+
'description': getattr(model, 'description', ''),
24+
'context_length': getattr(model, 'context_length', None)
25+
})
26+
27+
return jsonify({'models': models})
28+
except Exception as e:
29+
app.logger.error("Error fetching models: %s", str(e), exc_info=True)
30+
return jsonify({'error': str(e)}), 500
31+
32+

routes/asn.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,6 @@ def api_v1_asn_advertised_prefixes(asn):
347347
finally:
348348
close_db(db)
349349

350-
@asn_blueprint.route("/<int:asn>")
351-
#@caching(timeout=86400) # 24 hours
350+
@asn_blueprint.route('/<int:asn>')
352351
def asn(asn):
353-
return render_template('pages/asn.html', asn=asn)
354-
355-
# Register blueprints
356-
def init_app(app):
357-
app.register_blueprint(asn_blueprint)
358-
app.register_blueprint(asn_api_v1_blueprint)
352+
return render_template('pages/asn.html', asn=asn)

templates/base.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
<!-- Analytics -->
6060
<script defer data-domain="bgp-data.net" src="https://plausible.io/js/script.outbound-links.tagged-events.js"></script>
6161
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
62-
<script type="text/javascript" src="https://a.usbrowserspeed.com/cs?pid=ddae2e0bce828a30a7b24f94f87290780f71120eaf9f11353f234c3bd86512d3&puid=68dbaa52e5531cd2ffc2c510"></script>
6362

6463
<!-- Additional headers -->
6564
{% block headers %}{% endblock %}

0 commit comments

Comments
 (0)