feat: add multilingual chat support with markdown rendering
Some checks failed
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (api) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (chat) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (frontend) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (worker) (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint Code (push) Has started running
CI/CD Pipeline / Generate Documentation (push) Has started running
Some checks failed
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (api) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (chat) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (frontend) (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (worker) (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint Code (push) Has started running
CI/CD Pipeline / Generate Documentation (push) Has started running
- Fix Socket.IO proxy configuration in nginx for chat connectivity - Add Socket.IO path routing (/socket.io/) with WebSocket upgrade support - Fix frontend healthcheck to use curl instead of wget - Add react-markdown and remark-gfm for proper markdown rendering - Implement language selector in chat interface (8 languages supported) - Add language parameter to chat agent and LLM prompts - Support English, Italian, Spanish, French, German, Portuguese, Chinese, Japanese This resolves the chat connection issues and enables users to receive AI responses in their preferred language with properly formatted markdown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -33,9 +33,9 @@ COPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
|||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Health check
|
# Health check (use curl which is available in nginx:alpine)
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
CMD curl -f http://127.0.0.1/health || exit 1
|
||||||
|
|
||||||
# Run nginx
|
# Run nginx
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -36,7 +36,23 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket for chat
|
# Socket.IO for chat (Socket.IO uses /socket.io/ path by default)
|
||||||
|
location /socket.io/ {
|
||||||
|
proxy_pass http://chat:8001/socket.io/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Socket.IO specific settings
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket for chat (alternative endpoint)
|
||||||
location /ws/ {
|
location /ws/ {
|
||||||
proxy_pass http://chat:8001/;
|
proxy_pass http://chat:8001/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"socket.io-client": "^4.6.0",
|
"socket.io-client": "^4.6.0",
|
||||||
"markdown-it": "^14.0.0",
|
"react-markdown": "^9.0.1",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
"date-fns": "^3.0.0"
|
"date-fns": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,20 +3,25 @@ import {
|
|||||||
AppBar, Toolbar, Typography, Container, Box, Paper,
|
AppBar, Toolbar, Typography, Container, Box, Paper,
|
||||||
TextField, Button, List, ListItem, ListItemText,
|
TextField, Button, List, ListItem, ListItemText,
|
||||||
CircularProgress, Chip, Grid, Card, CardContent,
|
CircularProgress, Chip, Grid, Card, CardContent,
|
||||||
Tabs, Tab, Divider, IconButton
|
Tabs, Tab, Divider, IconButton, Select, MenuItem,
|
||||||
|
FormControl, InputLabel
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Send as SendIcon,
|
Send as SendIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Description as DocIcon,
|
Description as DocIcon,
|
||||||
Support as SupportIcon,
|
Support as SupportIcon,
|
||||||
CloudUpload as UploadIcon
|
CloudUpload as UploadIcon,
|
||||||
|
Language as LanguageIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
// Use relative URLs to work with nginx proxy in production
|
||||||
const CHAT_URL = import.meta.env.VITE_CHAT_URL || 'http://localhost:8001';
|
const API_URL = import.meta.env.VITE_API_URL || (typeof window !== 'undefined' ? window.location.origin + '/api' : 'http://localhost:8000');
|
||||||
|
const CHAT_URL = import.meta.env.VITE_CHAT_URL || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8001');
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
@@ -56,6 +61,7 @@ function ChatInterface() {
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [socket, setSocket] = useState(null);
|
const [socket, setSocket] = useState(null);
|
||||||
|
const [language, setLanguage] = useState('en');
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,7 +97,7 @@ function ChatInterface() {
|
|||||||
setMessages(prev => [...prev, userMessage]);
|
setMessages(prev => [...prev, userMessage]);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
socket.emit('chat', { message: input, history: messages });
|
socket.emit('chat', { message: input, history: messages, language: language });
|
||||||
setInput('');
|
setInput('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,11 +105,32 @@ function ChatInterface() {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} md={8}>
|
<Grid item xs={12} md={8}>
|
||||||
<Paper sx={{ height: '70vh', display: 'flex', flexDirection: 'column' }}>
|
<Paper sx={{ height: '70vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'white' }}>
|
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'white', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Typography variant="h6">Technical Support Chat</Typography>
|
<Box>
|
||||||
<Typography variant="caption">
|
<Typography variant="h6">Technical Support Chat</Typography>
|
||||||
AI-powered assistant with access to datacenter documentation
|
<Typography variant="caption">
|
||||||
</Typography>
|
AI-powered assistant with access to datacenter documentation
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120, bgcolor: 'white', borderRadius: 1 }}>
|
||||||
|
<InputLabel id="language-select-label">Language</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="language-select-label"
|
||||||
|
value={language}
|
||||||
|
label="Language"
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
startAdornment={<LanguageIcon sx={{ mr: 0.5, color: 'action.active' }} />}
|
||||||
|
>
|
||||||
|
<MenuItem value="en">🇬🇧 English</MenuItem>
|
||||||
|
<MenuItem value="it">🇮🇹 Italiano</MenuItem>
|
||||||
|
<MenuItem value="es">🇪🇸 Español</MenuItem>
|
||||||
|
<MenuItem value="fr">🇫🇷 Français</MenuItem>
|
||||||
|
<MenuItem value="de">🇩🇪 Deutsch</MenuItem>
|
||||||
|
<MenuItem value="pt">🇵🇹 Português</MenuItem>
|
||||||
|
<MenuItem value="zh">🇨🇳 中文</MenuItem>
|
||||||
|
<MenuItem value="ja">🇯🇵 日本語</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
||||||
@@ -117,7 +144,38 @@ function ChatInterface() {
|
|||||||
maxWidth: '70%',
|
maxWidth: '70%',
|
||||||
bgcolor: msg.role === 'user' ? 'primary.light' : 'grey.100'
|
bgcolor: msg.role === 'user' ? 'primary.light' : 'grey.100'
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="body1">{msg.content}</Typography>
|
{msg.role === 'user' ? (
|
||||||
|
<Typography variant="body1">{msg.content}</Typography>
|
||||||
|
) : (
|
||||||
|
<Box sx={{
|
||||||
|
'& h1, & h2, & h3': { mt: 2, mb: 1 },
|
||||||
|
'& h1': { fontSize: '1.5rem', fontWeight: 600 },
|
||||||
|
'& h2': { fontSize: '1.3rem', fontWeight: 600 },
|
||||||
|
'& h3': { fontSize: '1.1rem', fontWeight: 600 },
|
||||||
|
'& p': { mb: 1 },
|
||||||
|
'& ul, & ol': { pl: 2, mb: 1 },
|
||||||
|
'& li': { mb: 0.5 },
|
||||||
|
'& code': {
|
||||||
|
bgcolor: 'rgba(0,0,0,0.05)',
|
||||||
|
p: 0.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.9em'
|
||||||
|
},
|
||||||
|
'& pre': {
|
||||||
|
bgcolor: 'rgba(0,0,0,0.05)',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
'& code': { bgcolor: 'transparent', p: 0 }
|
||||||
|
},
|
||||||
|
'& hr': { my: 2, border: 'none', borderTop: '1px solid rgba(0,0,0,0.12)' }
|
||||||
|
}}>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{msg.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{msg.related_docs && (
|
{msg.related_docs && (
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
{msg.related_docs.map((doc, i) => (
|
{msg.related_docs.map((doc, i) => (
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from langchain.embeddings import HuggingFaceEmbeddings
|
from langchain_community.embeddings import HuggingFaceEmbeddings
|
||||||
|
from langchain_community.vectorstores import Chroma
|
||||||
from langchain.schema import Document
|
from langchain.schema import Document
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
from langchain.vectorstores import Chroma
|
|
||||||
|
|
||||||
from ..mcp.client import MCPClient
|
from ..mcp.client import MCPClient
|
||||||
from ..utils.llm_client import LLMClient
|
from ..utils.llm_client import LLMClient
|
||||||
@@ -27,7 +27,7 @@ class DocumentationAgent:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mcp_client: MCPClient,
|
mcp_client: Optional[MCPClient] = None,
|
||||||
llm_client: Optional[LLMClient] = None,
|
llm_client: Optional[LLMClient] = None,
|
||||||
vector_store_path: str = "./data/chroma_db",
|
vector_store_path: str = "./data/chroma_db",
|
||||||
):
|
):
|
||||||
@@ -35,7 +35,7 @@ class DocumentationAgent:
|
|||||||
Initialize Documentation Agent.
|
Initialize Documentation Agent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mcp_client: MCP client for infrastructure connectivity
|
mcp_client: MCP client for infrastructure connectivity (optional for chat-only mode)
|
||||||
llm_client: LLM client (uses default if not provided)
|
llm_client: LLM client (uses default if not provided)
|
||||||
vector_store_path: Path to vector store directory
|
vector_store_path: Path to vector store directory
|
||||||
"""
|
"""
|
||||||
@@ -43,11 +43,17 @@ class DocumentationAgent:
|
|||||||
self.client = llm_client or LLMClient()
|
self.client = llm_client or LLMClient()
|
||||||
self.vector_store_path = Path(vector_store_path)
|
self.vector_store_path = Path(vector_store_path)
|
||||||
|
|
||||||
# Initialize embeddings and vector store
|
# Initialize embeddings and vector store (optional - for RAG functionality)
|
||||||
self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
self.embeddings: Optional[HuggingFaceEmbeddings] = None
|
||||||
|
self.vector_store: Optional[Chroma] = None
|
||||||
|
|
||||||
self.vector_store: Chroma
|
try:
|
||||||
self._load_vector_store()
|
self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
||||||
|
self._load_vector_store()
|
||||||
|
logger.info("Vector store initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Vector store not available (will use LLM without RAG): {e}")
|
||||||
|
# Agent will work without document search, using only LLM knowledge
|
||||||
|
|
||||||
def _load_vector_store(self) -> None:
|
def _load_vector_store(self) -> None:
|
||||||
"""Load or create vector store"""
|
"""Load or create vector store"""
|
||||||
@@ -119,18 +125,25 @@ class DocumentationAgent:
|
|||||||
Returns:
|
Returns:
|
||||||
List of relevant documentation chunks with metadata
|
List of relevant documentation chunks with metadata
|
||||||
"""
|
"""
|
||||||
|
# If vector store not available, return empty list
|
||||||
|
if self.vector_store is None:
|
||||||
|
logger.warning("⚠️ Vector store not available, skipping document search")
|
||||||
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"🔍 Searching documentation for: '{query[:100]}'...")
|
||||||
|
|
||||||
# Build filter if sections specified
|
# Build filter if sections specified
|
||||||
filter_dict = None
|
filter_dict = None
|
||||||
if sections:
|
if sections:
|
||||||
filter_dict = {"section": {"$in": sections}}
|
filter_dict = {"section": {"$in": sections}}
|
||||||
|
|
||||||
# Perform similarity search
|
# Perform similarity search
|
||||||
results: list[Any] = []
|
results: list[Any] = self.vector_store.similarity_search_with_score(
|
||||||
if self.vector_store is not None:
|
query=query, k=limit, filter=filter_dict # type: ignore[arg-type]
|
||||||
results = self.vector_store.similarity_search_with_score(
|
)
|
||||||
query=query, k=limit, filter=filter_dict # type: ignore[arg-type]
|
|
||||||
)
|
logger.info(f"📚 Found {len(results)} relevant document chunks")
|
||||||
|
|
||||||
# Format results
|
# Format results
|
||||||
formatted_results = []
|
formatted_results = []
|
||||||
@@ -144,11 +157,12 @@ class DocumentationAgent:
|
|||||||
"last_updated": doc.metadata.get("indexed_at", ""),
|
"last_updated": doc.metadata.get("indexed_at", ""),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
logger.info(f" ✓ Section: {doc.metadata.get('section')} (relevance: {(1-score)*100:.1f}%)")
|
||||||
|
|
||||||
return formatted_results
|
return formatted_results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Documentation search failed: {e}")
|
logger.error(f"❌ Documentation search failed: {e}", exc_info=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def resolve_ticket(
|
async def resolve_ticket(
|
||||||
@@ -265,7 +279,7 @@ Respond in JSON format:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def chat_with_context(
|
async def chat_with_context(
|
||||||
self, user_message: str, conversation_history: List[Dict[str, str]]
|
self, user_message: str, conversation_history: List[Dict[str, str]], language: str = "en"
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Chat with user while autonomously searching documentation
|
Chat with user while autonomously searching documentation
|
||||||
@@ -273,6 +287,7 @@ Respond in JSON format:
|
|||||||
Args:
|
Args:
|
||||||
user_message: User's message
|
user_message: User's message
|
||||||
conversation_history: Previous messages
|
conversation_history: Previous messages
|
||||||
|
language: Language code for response (en, it, es, fr, de, pt, zh, ja)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response with documentation references
|
Response with documentation references
|
||||||
@@ -284,6 +299,23 @@ Respond in JSON format:
|
|||||||
# Build context
|
# Build context
|
||||||
context = self._build_context(relevant_docs)
|
context = self._build_context(relevant_docs)
|
||||||
|
|
||||||
|
# Language names mapping
|
||||||
|
language_names = {
|
||||||
|
"en": "English",
|
||||||
|
"it": "Italian",
|
||||||
|
"es": "Spanish",
|
||||||
|
"fr": "French",
|
||||||
|
"de": "German",
|
||||||
|
"pt": "Portuguese",
|
||||||
|
"zh": "Chinese",
|
||||||
|
"ja": "Japanese"
|
||||||
|
}
|
||||||
|
|
||||||
|
language_instruction = ""
|
||||||
|
if language != "en":
|
||||||
|
lang_name = language_names.get(language, language)
|
||||||
|
language_instruction = f"\n\n**IMPORTANT: You MUST respond in {lang_name}. Translate your entire response to {lang_name}.**"
|
||||||
|
|
||||||
# Build conversation
|
# Build conversation
|
||||||
system_prompt = f"""You are a helpful datacenter technical support assistant. You have access to comprehensive datacenter documentation.
|
system_prompt = f"""You are a helpful datacenter technical support assistant. You have access to comprehensive datacenter documentation.
|
||||||
|
|
||||||
@@ -296,6 +328,7 @@ When answering questions:
|
|||||||
|
|
||||||
**Available Documentation Context:**
|
**Available Documentation Context:**
|
||||||
{context}
|
{context}
|
||||||
|
{language_instruction}
|
||||||
|
|
||||||
Answer naturally and helpfully."""
|
Answer naturally and helpfully."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,94 @@
|
|||||||
"""
|
"""
|
||||||
Chat server stub for development.
|
Chat server with Socket.IO support for real-time communication.
|
||||||
|
|
||||||
TODO: Implement full chat server with WebSocket support.
|
|
||||||
Currently this is a minimal stub to allow the development environment to start.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import socketio
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from .agent import DocumentationAgent
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Global agent instance
|
||||||
|
agent = None
|
||||||
|
|
||||||
|
|
||||||
|
async def index_documentation_if_needed() -> None:
|
||||||
|
"""Index documentation on first startup if needed."""
|
||||||
|
vector_store_path = Path("/app/data/chroma_db")
|
||||||
|
index_marker = vector_store_path / ".indexed"
|
||||||
|
docs_path = Path("/app/output")
|
||||||
|
|
||||||
|
if not index_marker.exists() and docs_path.exists():
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("First Time Setup - Indexing Documentation")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("This may take a few minutes...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create temporary agent for indexing
|
||||||
|
temp_agent = DocumentationAgent(
|
||||||
|
mcp_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
vector_store_path=str(vector_store_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Index documentation
|
||||||
|
await temp_agent.index_documentation(docs_path)
|
||||||
|
|
||||||
|
# Create marker
|
||||||
|
vector_store_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
index_marker.touch()
|
||||||
|
|
||||||
|
logger.info("✓ Documentation indexed successfully!")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to index documentation: {e}", exc_info=True)
|
||||||
|
logger.warning("Chat will work with limited functionality")
|
||||||
|
else:
|
||||||
|
if index_marker.exists():
|
||||||
|
logger.info("✓ Vector store already initialized")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Documentation path not found: {docs_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize Documentation Agent (without MCP for chat-only mode)
|
||||||
|
async def initialize_agent() -> None:
|
||||||
|
"""Initialize the documentation agent."""
|
||||||
|
global agent
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Index documentation if needed
|
||||||
|
await index_documentation_if_needed()
|
||||||
|
|
||||||
|
# Create agent with vector store access
|
||||||
|
agent = DocumentationAgent(mcp_client=None) # type: ignore
|
||||||
|
logger.info("Documentation Agent initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to initialize Documentation Agent: {e}")
|
||||||
|
agent = None
|
||||||
|
|
||||||
|
# Create Socket.IO server
|
||||||
|
sio = socketio.AsyncServer(
|
||||||
|
async_mode="asgi",
|
||||||
|
cors_allowed_origins="*",
|
||||||
|
logger=True,
|
||||||
|
engineio_logger=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Datacenter Documentation Chat Server",
|
title="Datacenter Documentation Chat Server",
|
||||||
description="WebSocket-based chat interface for documentation queries (STUB - NOT IMPLEMENTED)",
|
description="WebSocket-based chat interface for documentation queries",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,31 +102,134 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Startup event
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event() -> None:
|
||||||
|
"""Initialize agent on startup."""
|
||||||
|
await initialize_agent()
|
||||||
|
|
||||||
|
|
||||||
|
# Mount Socket.IO to FastAPI
|
||||||
|
socket_app = socketio.ASGIApp(sio, app)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check() -> dict[str, str | bool]:
|
async def health_check() -> dict[str, str | bool]:
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
return {"status": "ok", "service": "chat", "implemented": False}
|
return {"status": "ok", "service": "chat", "implemented": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root() -> dict[str, str]:
|
async def root() -> dict[str, str]:
|
||||||
"""Root endpoint."""
|
"""Root endpoint."""
|
||||||
return {
|
return {
|
||||||
"message": "Chat server stub - not yet implemented",
|
"message": "Chat server with Socket.IO support",
|
||||||
"status": "stub",
|
"status": "active",
|
||||||
"todo": "Implement WebSocket chat functionality",
|
"socket_io": "enabled",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement WebSocket endpoint for chat
|
# Socket.IO event handlers
|
||||||
# @app.websocket("/ws")
|
@sio.event
|
||||||
# async def websocket_endpoint(websocket: WebSocket):
|
async def connect(sid: str, environ: Dict[str, Any]) -> None:
|
||||||
# await websocket.accept()
|
"""Handle client connection."""
|
||||||
# # Implement chat logic here
|
logger.info(f"Client connected: {sid}")
|
||||||
|
await sio.emit("connection_response", {"status": "connected", "sid": sid}, room=sid)
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def disconnect(sid: str) -> None:
|
||||||
|
"""Handle client disconnection."""
|
||||||
|
logger.info(f"Client disconnected: {sid}")
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def message(sid: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""Handle incoming chat messages."""
|
||||||
|
logger.info(f"Message from {sid}: {data}")
|
||||||
|
|
||||||
|
# Echo the message back for now (TODO: integrate with DocumentationAgent)
|
||||||
|
response = {
|
||||||
|
"message": f"Received: {data.get('message', '')}",
|
||||||
|
"timestamp": data.get("timestamp"),
|
||||||
|
"type": "echo",
|
||||||
|
}
|
||||||
|
|
||||||
|
await sio.emit("message", response, room=sid)
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def chat_message(sid: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""Handle chat messages (alternative event name)."""
|
||||||
|
logger.info(f"Chat message from {sid}: {data}")
|
||||||
|
|
||||||
|
# TODO: Integrate with DocumentationAgent for intelligent responses
|
||||||
|
user_message = data.get("message", "")
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"message": f"Chat server received: {user_message}",
|
||||||
|
"timestamp": data.get("timestamp"),
|
||||||
|
"type": "response",
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
|
||||||
|
await sio.emit("chat_response", response, room=sid)
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def chat(sid: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""Handle chat event from frontend."""
|
||||||
|
logger.info(f"Chat event from {sid}: {data}")
|
||||||
|
|
||||||
|
user_message = data.get("message", "")
|
||||||
|
conversation_history = data.get("history", [])
|
||||||
|
language = data.get("language", "en")
|
||||||
|
|
||||||
|
# Use Documentation Agent for intelligent responses
|
||||||
|
if agent is not None:
|
||||||
|
try:
|
||||||
|
# Convert frontend history format to agent format
|
||||||
|
agent_history = []
|
||||||
|
for msg in conversation_history:
|
||||||
|
role = "assistant" if msg.get("role") == "assistant" else "user"
|
||||||
|
agent_history.append({"role": role, "content": msg.get("content", "")})
|
||||||
|
|
||||||
|
# Get AI response
|
||||||
|
ai_response = await agent.chat_with_context(
|
||||||
|
user_message=user_message, conversation_history=agent_history, language=language
|
||||||
|
)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"message": ai_response.get("message", "I apologize, I couldn't generate a response."),
|
||||||
|
"type": "bot",
|
||||||
|
"timestamp": data.get("timestamp"),
|
||||||
|
"related_docs": ai_response.get("related_docs", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in agent response: {e}", exc_info=True)
|
||||||
|
response = {
|
||||||
|
"message": f"I apologize, but I encountered an error: {str(e)}. Please try again.",
|
||||||
|
"type": "bot",
|
||||||
|
"timestamp": data.get("timestamp"),
|
||||||
|
"related_docs": [],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Fallback if agent not initialized
|
||||||
|
response = {
|
||||||
|
"message": f"Echo (Agent not initialized): {user_message}",
|
||||||
|
"type": "bot",
|
||||||
|
"timestamp": data.get("timestamp"),
|
||||||
|
"related_docs": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frontend listens to 'message' event
|
||||||
|
await sio.emit("message", response, room=sid)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
logger.info("Starting chat server stub on port 8001...")
|
logger.info("Starting chat server with Socket.IO on port 8001...")
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
# Use socket_app instead of app to enable Socket.IO
|
||||||
|
uvicorn.run(socket_app, host="0.0.0.0", port=8001)
|
||||||
|
|||||||
Reference in New Issue
Block a user