2025-12-25 14:54:33 +00:00
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import uuid
|
2025-12-26 16:48:51 +00:00
|
|
|
import shutil
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
import chainlit as cl
|
2025-12-26 09:11:06 +00:00
|
|
|
import ollama
|
2025-12-26 09:58:49 +00:00
|
|
|
from qdrant_client import AsyncQdrantClient
|
|
|
|
|
from qdrant_client.models import PointStruct, Distance, VectorParams
|
2025-12-26 10:07:15 +00:00
|
|
|
from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
|
|
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === CONFIGURAZIONE ===
|
|
|
|
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station")
|
|
|
|
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://192.168.1.243:11434")
|
|
|
|
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
2025-12-26 09:11:06 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === INIZIALIZZAZIONE DATA LAYER ===
|
|
|
|
|
# IMPORTANTE: Deve essere inizializzato PRIMA di definire gli handlers
|
|
|
|
|
try:
|
|
|
|
|
data_layer = SQLAlchemyDataLayer(conninfo=DATABASE_URL)
|
|
|
|
|
cl.data_layer = data_layer
|
|
|
|
|
print("✅ SQLAlchemyDataLayer initialized successfully")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"❌ Failed to initialize data layer: {e}")
|
|
|
|
|
cl.data_layer = None
|
2025-12-25 18:00:13 +00:00
|
|
|
|
2025-12-26 07:45:40 +00:00
|
|
|
WORKSPACES_DIR = "./workspaces"
|
2025-12-26 16:48:51 +00:00
|
|
|
USER_ROLE = "admin"
|
2025-12-25 18:00:13 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === UTILITY FUNCTIONS ===
|
|
|
|
|
def create_workspace(user_role: str):
|
|
|
|
|
"""Crea directory workspace se non esiste"""
|
2025-12-26 07:45:40 +00:00
|
|
|
workspace_path = os.path.join(WORKSPACES_DIR, user_role)
|
2025-12-26 16:48:51 +00:00
|
|
|
os.makedirs(workspace_path, exist_ok=True)
|
|
|
|
|
return workspace_path
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
def save_code_to_file(code: str, user_role: str) -> str:
|
|
|
|
|
"""Salva blocco codice come file .py"""
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
2025-12-25 14:54:33 +00:00
|
|
|
file_name = f"code_{timestamp}.py"
|
|
|
|
|
file_path = os.path.join(WORKSPACES_DIR, user_role, file_name)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
|
|
|
f.write(code)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-25 14:54:33 +00:00
|
|
|
return file_path
|
|
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === QDRANT FUNCTIONS ===
|
|
|
|
|
async def get_qdrant_client() -> AsyncQdrantClient:
|
|
|
|
|
"""Connessione a Qdrant"""
|
|
|
|
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
2025-12-26 07:45:40 +00:00
|
|
|
collection_name = "documents"
|
|
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# Crea collection se non esiste
|
2025-12-26 09:58:49 +00:00
|
|
|
if not await client.collection_exists(collection_name):
|
|
|
|
|
await client.create_collection(
|
2025-12-26 07:45:40 +00:00
|
|
|
collection_name=collection_name,
|
2025-12-26 09:58:49 +00:00
|
|
|
vectors_config=VectorParams(size=768, distance=Distance.COSINE)
|
2025-12-25 14:54:33 +00:00
|
|
|
)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
async def get_embeddings(text: str) -> list:
|
|
|
|
|
"""Genera embeddings con Ollama"""
|
2025-12-26 09:58:49 +00:00
|
|
|
client = ollama.Client(host=OLLAMA_URL)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# Limita lunghezza per evitare errori
|
|
|
|
|
max_length = 2000
|
|
|
|
|
if len(text) > max_length:
|
|
|
|
|
text = text[:max_length]
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 09:11:06 +00:00
|
|
|
try:
|
|
|
|
|
response = client.embed(model='nomic-embed-text', input=text)
|
2025-12-26 16:48:51 +00:00
|
|
|
|
2025-12-26 09:11:06 +00:00
|
|
|
if 'embeddings' in response:
|
|
|
|
|
return response['embeddings'][0]
|
2025-12-26 16:48:51 +00:00
|
|
|
return response.get('embedding', [])
|
|
|
|
|
|
2025-12-26 09:11:06 +00:00
|
|
|
except Exception as e:
|
2025-12-26 16:48:51 +00:00
|
|
|
print(f"❌ Errore Embedding: {e}")
|
2025-12-26 09:11:06 +00:00
|
|
|
return []
|
2025-12-25 18:00:13 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
async def index_document(file_name: str, content: str) -> bool:
|
|
|
|
|
"""Indicizza documento su Qdrant"""
|
2025-12-26 07:45:40 +00:00
|
|
|
try:
|
2025-12-26 16:48:51 +00:00
|
|
|
embeddings = await get_embeddings(content)
|
|
|
|
|
if not embeddings:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
qdrant_client = await get_qdrant_client()
|
|
|
|
|
point_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
point = PointStruct(
|
|
|
|
|
id=point_id,
|
|
|
|
|
vector=embeddings,
|
|
|
|
|
payload={
|
|
|
|
|
"file_name": file_name,
|
|
|
|
|
"content": content,
|
|
|
|
|
"indexed_at": datetime.now().isoformat()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await qdrant_client.upsert(collection_name="documents", points=[point])
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"❌ Errore indicizzazione: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def search_qdrant(query_text: str, limit: int = 3) -> str:
|
|
|
|
|
"""Ricerca documenti rilevanti"""
|
|
|
|
|
try:
|
|
|
|
|
qdrant_client = await get_qdrant_client()
|
2025-12-26 07:45:40 +00:00
|
|
|
query_embedding = await get_embeddings(query_text)
|
|
|
|
|
|
2025-12-26 09:11:06 +00:00
|
|
|
if not query_embedding:
|
|
|
|
|
return ""
|
2025-12-26 16:48:51 +00:00
|
|
|
|
2025-12-26 09:58:49 +00:00
|
|
|
search_result = await qdrant_client.query_points(
|
2025-12-26 07:45:40 +00:00
|
|
|
collection_name="documents",
|
2025-12-26 09:58:49 +00:00
|
|
|
query=query_embedding,
|
2025-12-26 16:48:51 +00:00
|
|
|
limit=limit
|
2025-12-26 07:45:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
contexts = []
|
2025-12-26 16:48:51 +00:00
|
|
|
for hit in search_result.points:
|
|
|
|
|
if hit.payload:
|
|
|
|
|
file_name = hit.payload.get('file_name', 'Unknown')
|
|
|
|
|
content = hit.payload.get('content', '')
|
|
|
|
|
score = hit.score if hasattr(hit, 'score') else 0
|
|
|
|
|
|
|
|
|
|
contexts.append(
|
|
|
|
|
f"📄 **{file_name}** (relevance: {score:.2f})\n"
|
|
|
|
|
f"``````"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return "\n\n".join(contexts) if contexts else ""
|
|
|
|
|
|
2025-12-26 07:45:40 +00:00
|
|
|
except Exception as e:
|
2025-12-26 16:48:51 +00:00
|
|
|
print(f"❌ Errore ricerca Qdrant: {e}")
|
2025-12-26 07:45:40 +00:00
|
|
|
return ""
|
2025-12-25 18:00:13 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === CHAINLIT HANDLERS ===
|
2025-12-25 14:54:33 +00:00
|
|
|
@cl.on_chat_start
|
2025-12-26 16:48:51 +00:00
|
|
|
async def on_chat_start():
|
|
|
|
|
"""Inizializzazione chat"""
|
|
|
|
|
create_workspace(USER_ROLE)
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# Imposta variabili sessione
|
|
|
|
|
cl.user_session.set("role", USER_ROLE)
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# Verifica persistenza
|
|
|
|
|
persistence_status = "✅ Attiva" if cl.data_layer else "⚠️ Disattivata"
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
await cl.Message(
|
|
|
|
|
content=f"🚀 **AI Station Ready** - Workspace: `{USER_ROLE}`\n\n"
|
|
|
|
|
f"📤 Upload `.txt` files per indicizzarli nel RAG\n"
|
|
|
|
|
f"💾 Persistenza conversazioni: {persistence_status}\n"
|
|
|
|
|
f"🤖 Modello: `qwen2.5-coder:7b` @ {OLLAMA_URL}"
|
|
|
|
|
).send()
|
2025-12-25 14:54:33 +00:00
|
|
|
|
|
|
|
|
@cl.on_message
|
2025-12-26 16:48:51 +00:00
|
|
|
async def on_message(message: cl.Message):
|
|
|
|
|
"""Gestione messaggi utente"""
|
|
|
|
|
user_role = cl.user_session.get("role", "guest")
|
2025-12-26 07:45:40 +00:00
|
|
|
|
|
|
|
|
try:
|
2025-12-26 16:48:51 +00:00
|
|
|
# === STEP 1: Gestione Upload ===
|
2025-12-26 07:45:40 +00:00
|
|
|
if message.elements:
|
2025-12-26 16:48:51 +00:00
|
|
|
await handle_file_uploads(message.elements, user_role)
|
|
|
|
|
|
|
|
|
|
# === STEP 2: RAG Search ===
|
|
|
|
|
context_text = await search_qdrant(message.content, limit=3)
|
|
|
|
|
|
|
|
|
|
# === STEP 3: Preparazione Prompt ===
|
|
|
|
|
messages = []
|
2025-12-26 09:58:49 +00:00
|
|
|
|
|
|
|
|
if context_text:
|
2025-12-26 16:48:51 +00:00
|
|
|
system_prompt = (
|
|
|
|
|
"Sei un assistente AI esperto. Usa ESCLUSIVAMENTE il seguente contesto "
|
|
|
|
|
"per rispondere. Se la risposta non è nel contesto, dillo chiaramente.\n\n"
|
|
|
|
|
f"**CONTESTO:**\n{context_text}"
|
|
|
|
|
)
|
|
|
|
|
messages.append({"role": "system", "content": system_prompt})
|
2025-12-26 09:58:49 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
messages.append({"role": "user", "content": message.content})
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === STEP 4: Chiamata Ollama con Streaming ===
|
|
|
|
|
client = ollama.Client(host=OLLAMA_URL)
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
msg = cl.Message(content="")
|
|
|
|
|
await msg.send()
|
2025-12-25 14:54:33 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
stream = client.chat(
|
|
|
|
|
model='qwen2.5-coder:7b',
|
|
|
|
|
messages=messages,
|
|
|
|
|
stream=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
full_response = ""
|
|
|
|
|
for chunk in stream:
|
|
|
|
|
content = chunk['message']['content']
|
|
|
|
|
full_response += content
|
|
|
|
|
await msg.stream_token(content)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
await msg.update()
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
# === STEP 5: Estrai e Salva Codice ===
|
|
|
|
|
code_blocks = re.findall(r"``````", full_response, re.DOTALL)
|
2025-12-26 07:45:40 +00:00
|
|
|
|
2025-12-26 16:48:51 +00:00
|
|
|
if code_blocks:
|
|
|
|
|
elements = []
|
|
|
|
|
for code in code_blocks:
|
|
|
|
|
file_path = save_code_to_file(code.strip(), user_role)
|
|
|
|
|
elements.append(
|
|
|
|
|
cl.File(
|
|
|
|
|
name=os.path.basename(file_path),
|
|
|
|
|
path=file_path,
|
|
|
|
|
display="inline"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await cl.Message(
|
|
|
|
|
content=f"💾 Codice salvato in `{user_role}/`",
|
|
|
|
|
elements=elements
|
|
|
|
|
).send()
|
|
|
|
|
|
2025-12-25 14:54:33 +00:00
|
|
|
except Exception as e:
|
2025-12-26 16:48:51 +00:00
|
|
|
await cl.Message(content=f"❌ **Errore:** {str(e)}").send()
|
|
|
|
|
|
|
|
|
|
async def handle_file_uploads(elements, user_role: str):
|
|
|
|
|
"""Gestisce upload e indicizzazione file"""
|
|
|
|
|
for element in elements:
|
|
|
|
|
try:
|
|
|
|
|
# Salva file
|
|
|
|
|
dest_path = os.path.join(WORKSPACES_DIR, user_role, element.name)
|
|
|
|
|
shutil.copy(element.path, dest_path)
|
|
|
|
|
|
|
|
|
|
# Indicizza solo .txt
|
|
|
|
|
if element.name.endswith('.txt'):
|
|
|
|
|
with open(dest_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
|
|
success = await index_document(element.name, content)
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
await cl.Message(
|
|
|
|
|
content=f"✅ **{element.name}** indicizzato in Qdrant"
|
|
|
|
|
).send()
|
|
|
|
|
else:
|
|
|
|
|
await cl.Message(
|
|
|
|
|
content=f"⚠️ Errore indicizzazione {element.name}"
|
|
|
|
|
).send()
|
|
|
|
|
else:
|
|
|
|
|
await cl.Message(
|
|
|
|
|
content=f"📁 **{element.name}** salvato (solo .txt vengono indicizzati)"
|
|
|
|
|
).send()
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
await cl.Message(
|
|
|
|
|
content=f"❌ Errore con {element.name}: {str(e)}"
|
|
|
|
|
).send()
|