351 lines
12 KiB
Python
351 lines
12 KiB
Python
import chromadb
|
|
from flask import Flask, request, jsonify, send_from_directory
|
|
import os
|
|
import json
|
|
import requests
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__)
|
|
|
|
# ChromaDB client setup
|
|
try:
|
|
client = chromadb.HttpClient(host="chromadb", port=8000)
|
|
logger.info("Connected to ChromaDB successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to ChromaDB: {e}")
|
|
client = None
|
|
|
|
# Company facts database (in-memory for now, could be replaced with persistent storage)
|
|
company_facts = {
|
|
"Apple": {
|
|
"products": [
|
|
{"name": "iPhone", "release_date": "2007", "specifications": "Smartphone with iOS"},
|
|
{"name": "MacBook", "release_date": "2006", "specifications": "Laptop with M1 chip"},
|
|
{"name": "iPad", "release_date": "2010", "specifications": "Tablet with iOS"},
|
|
{"name": "Apple Watch", "release_date": "2015", "specifications": "Smartwatch with watchOS"}
|
|
],
|
|
"founded": "1976",
|
|
"ceo": "Tim Cook",
|
|
"headquarters": "Cupertino, California",
|
|
"ipo_year": "1980",
|
|
"market_cap": "$2.8T (2026)",
|
|
"key_executives": [
|
|
{"name": "Tim Cook", "position": "CEO"},
|
|
{"name": "Johny Srouji", "position": "CFO"},
|
|
{"name": "Katherine Adams", "position": "Chief Design Officer"}
|
|
],
|
|
"business_segments": ["Consumer Electronics", "Software", "Services"]
|
|
},
|
|
"Microsoft": {
|
|
"products": [
|
|
{"name": "Windows", "release_date": "1985", "specifications": "Operating system"},
|
|
{"name": "Office", "release_date": "1989", "specifications": "Productivity suite"},
|
|
{"name": "Azure", "release_date": "2010", "specifications": "Cloud computing platform"},
|
|
{"name": "Xbox", "release_date": "2001", "specifications": "Gaming console"}
|
|
],
|
|
"founded": "1975",
|
|
"ceo": "Satya Nadella",
|
|
"headquarters": "Redmond, Washington",
|
|
"ipo_year": "1986",
|
|
"market_cap": "$3.2T (2026)",
|
|
"key_executives": [
|
|
{"name": "Satya Nadella", "position": "CEO"},
|
|
{"name": "Amy Hood", "position": "CFO"},
|
|
{"name": "Kevin Scott", "position": "CTO"}
|
|
],
|
|
"business_segments": ["Software", "Cloud Services", "Gaming", "Productivity"]
|
|
},
|
|
"Google": {
|
|
"products": [
|
|
{"name": "Search Engine", "release_date": "1998", "specifications": "Web search platform"},
|
|
{"name": "Android", "release_date": "2008", "specifications": "Mobile operating system"},
|
|
{"name": "Gmail", "release_date": "2004", "specifications": "Email service"},
|
|
{"name": "YouTube", "release_date": "2005", "specifications": "Video sharing platform"}
|
|
],
|
|
"founded": "1998",
|
|
"ceo": "Sundar Pichai",
|
|
"headquarters": "Mountain View, California",
|
|
"ipo_year": "2004",
|
|
"market_cap": "$1.7T (2026)",
|
|
"key_executives": [
|
|
{"name": "Sundar Pichai", "position": "CEO"},
|
|
{"name": "Ruth Porat", "position": "CFO"},
|
|
{"name": "Rajen S. Suri", "position": "Chief Technology Officer"}
|
|
],
|
|
"business_segments": ["Search", "Advertising", "Cloud", "Mobile"]
|
|
},
|
|
"Amazon": {
|
|
"products": [
|
|
{"name": "Amazon Web Services (AWS)", "release_date": "2006", "specifications": "Cloud computing platform"},
|
|
{"name": "Kindle", "release_date": "2007", "specifications": "E-reader device"},
|
|
{"name": "Alexa", "release_date": "2014", "specifications": "Voice assistant"},
|
|
{"name": "Prime Video", "release_date": "2008", "specifications": "Streaming service"}
|
|
],
|
|
"founded": "1994",
|
|
"ceo": "Andy Jassy",
|
|
"headquarters": "Seattle, Washington",
|
|
"ipo_year": "1997",
|
|
"market_cap": "$1.5T (2026)",
|
|
"key_executives": [
|
|
{"name": "Andy Jassy", "position": "CEO"},
|
|
{"name": "Brian T. Olsavsky", "position": "CFO"},
|
|
{"name": "Wendy J. Smith", "position": "Chief Technology Officer"}
|
|
],
|
|
"business_segments": ["E-commerce", "Cloud Computing", "Digital Streaming", "Advertising"]
|
|
}
|
|
}
|
|
|
|
def get_embedding(text):
|
|
"""
|
|
Get embedding using the OpenAI-compatible server at http://example.com:4000
|
|
"""
|
|
try:
|
|
# Using the OpenAI-compatible endpoint for embeddings
|
|
response = requests.post(
|
|
"http://example.com:4000/v1/embeddings",
|
|
json={
|
|
"input": text,
|
|
"model": "text-embedding-3-small" # or whatever model you're using
|
|
},
|
|
timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
embedding = response.json()['data'][0]['embedding']
|
|
return embedding
|
|
except Exception as e:
|
|
logger.error(f"Error getting embedding: {e}")
|
|
return None
|
|
|
|
def get_diverse_articles(articles, max_diverse=5):
|
|
"""
|
|
Filter articles to ensure diversity in content, sources, and perspectives
|
|
"""
|
|
if len(articles) <= max_diverse:
|
|
return articles
|
|
|
|
# More sophisticated diversity algorithm
|
|
diverse_articles = []
|
|
source_count = {}
|
|
topic_count = {}
|
|
|
|
# First pass: try to get articles from different sources
|
|
for article in articles:
|
|
source = article.get('metadata', {}).get('source', 'unknown')
|
|
topic = article.get('metadata', {}).get('topic', 'unknown')
|
|
|
|
# If we haven't reached max diversity and this source is new, add it
|
|
if len(diverse_articles) < max_diverse and source not in source_count:
|
|
diverse_articles.append(article)
|
|
source_count[source] = 1
|
|
topic_count[topic] = topic_count.get(topic, 0) + 1
|
|
|
|
# Second pass: fill remaining slots with different topics if possible
|
|
if len(diverse_articles) < max_diverse:
|
|
for article in articles:
|
|
if len(diverse_articles) >= max_diverse:
|
|
break
|
|
source = article.get('metadata', {}).get('source', 'unknown')
|
|
topic = article.get('metadata', {}).get('topic', 'unknown')
|
|
|
|
# Add article if it's from a different topic and we haven't seen too many from this topic
|
|
if source not in source_count and topic_count.get(topic, 0) < 2:
|
|
diverse_articles.append(article)
|
|
source_count[source] = 1
|
|
topic_count[topic] = topic_count.get(topic, 0) + 1
|
|
|
|
# If we still don't have enough, just return first few
|
|
if len(diverse_articles) < max_diverse:
|
|
return articles[:max_diverse]
|
|
|
|
return diverse_articles
|
|
|
|
def query_chroma(question, n_results=10):
|
|
"""
|
|
Query ChromaDB for articles related to the question
|
|
"""
|
|
if not client:
|
|
return {"error": "ChromaDB connection failed"}
|
|
|
|
try:
|
|
# Get embedding for the question
|
|
query_embedding = get_embedding(question)
|
|
if not query_embedding:
|
|
return {"error": "Failed to get embedding"}
|
|
|
|
# Query the collection
|
|
results = client.get_or_create_collection("news").query(
|
|
query_embeddings=[query_embedding],
|
|
n_results=n_results,
|
|
)
|
|
|
|
return results
|
|
except Exception as e:
|
|
logger.error(f"Error querying ChromaDB: {e}")
|
|
return {"error": f"Query failed: {str(e)}"}
|
|
|
|
# New endpoints implementation based on the OpenAPI specification
|
|
|
|
@app.route("/query", methods=["POST"])
|
|
def query_vector_database():
|
|
"""Query the vector database"""
|
|
data = request.get_json()
|
|
question = data.get("question")
|
|
|
|
if not question:
|
|
return jsonify({"error": "Missing 'question' in request body"}), 400
|
|
|
|
# Query ChromaDB for relevant articles
|
|
results = query_chroma(question, n_results=10)
|
|
|
|
if "error" in results:
|
|
return jsonify({"error": results["error"]}), 500
|
|
|
|
# Process results to create diverse article set
|
|
mcp_results = []
|
|
for doc, score, meta in zip(
|
|
results.get("documents", [[]])[0],
|
|
results.get("distances", [[]])[0],
|
|
results.get("metadatas", [[]])[0]):
|
|
mcp_results.append({
|
|
"document": doc,
|
|
"score": float(score),
|
|
"metadata": meta
|
|
})
|
|
|
|
# Apply diversity filtering
|
|
diverse_results = get_diverse_articles(mcp_results, 5)
|
|
|
|
return jsonify({"results": diverse_results})
|
|
|
|
@app.route("/articles/query", methods=["POST"])
|
|
def query_articles():
|
|
"""Query articles based on a question with diversity filtering"""
|
|
data = request.get_json()
|
|
question = data.get("question")
|
|
max_results = data.get("max_results", 5)
|
|
|
|
if not question:
|
|
return jsonify({"error": "Missing 'question' in request body"}), 400
|
|
|
|
# Query ChromaDB for relevant articles
|
|
results = query_chroma(question, n_results=max_results)
|
|
|
|
if "error" in results:
|
|
return jsonify({"error": results["error"]}), 500
|
|
|
|
# Process results to create diverse article set
|
|
mcp_results = []
|
|
for doc, score, meta in zip(
|
|
results.get("documents", [[]])[0],
|
|
results.get("distances", [[]])[0],
|
|
results.get("metadatas", [[]])[0]):
|
|
mcp_results.append({
|
|
"document": doc,
|
|
"score": float(score),
|
|
"metadata": meta
|
|
})
|
|
|
|
# Apply diversity filtering
|
|
diverse_results = get_diverse_articles(mcp_results, max_results)
|
|
|
|
return jsonify({
|
|
"results": diverse_results,
|
|
"query": question
|
|
})
|
|
|
|
@app.route("/articles/latest/<field>", methods=["GET"])
|
|
def get_latest_articles(field):
|
|
"""Get latest articles about a specific field with diversity"""
|
|
# This would typically query the database for latest articles about the field
|
|
# For now, we'll return some sample data
|
|
sample_articles = [
|
|
{
|
|
"document": f"Latest article about {field}",
|
|
"score": 0.95,
|
|
"metadata": {
|
|
"source": "Sample Source",
|
|
"topic": field,
|
|
"date": "2026-01-31"
|
|
}
|
|
}
|
|
]
|
|
|
|
return jsonify({
|
|
"results": sample_articles,
|
|
"field": field
|
|
})
|
|
|
|
@app.route("/company/<company_name>/facts", methods=["GET"])
|
|
def get_company_facts(company_name):
|
|
"""Get facts about a specific company"""
|
|
facts = company_facts.get(company_name, {})
|
|
if not facts:
|
|
return jsonify({"error": f"Company {company_name} not found"}), 404
|
|
|
|
return jsonify({
|
|
"company": company_name,
|
|
"facts": facts
|
|
})
|
|
|
|
@app.route("/company/<company_name>/products", methods=["GET"])
|
|
def get_company_products(company_name):
|
|
"""Get products information for a company"""
|
|
facts = company_facts.get(company_name, {})
|
|
products = facts.get("products", [])
|
|
|
|
if not products:
|
|
return jsonify({"error": f"No products found for company {company_name}"}), 404
|
|
|
|
return jsonify({
|
|
"company": company_name,
|
|
"products": products
|
|
})
|
|
|
|
@app.route("/company/facts/update", methods=["POST"])
|
|
def update_company_facts():
|
|
"""Update or add company facts"""
|
|
data = request.get_json()
|
|
company_name = data.get("company_name")
|
|
facts = data.get("facts")
|
|
|
|
if not company_name or not facts:
|
|
return jsonify({"error": "Missing 'company_name' or 'facts' in request body"}), 400
|
|
|
|
# Update or add company facts
|
|
company_facts[company_name] = facts
|
|
|
|
return jsonify({
|
|
"message": "Facts updated successfully",
|
|
"company": company_name,
|
|
"facts": facts
|
|
})
|
|
|
|
@app.route("/health", methods=["GET"])
|
|
def health():
|
|
"""Health check endpoint"""
|
|
return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()})
|
|
|
|
@app.route("/info", methods=["GET"])
|
|
def info():
|
|
"""Service information endpoint"""
|
|
return jsonify({
|
|
"provider": "StockDoc",
|
|
"service": "MCP Server",
|
|
"version": "1.0.0",
|
|
"collection": "news",
|
|
"embedding_service": "http://example.com:4000"
|
|
})
|
|
|
|
@app.route("/openapi.json", methods=["GET"])
|
|
def openapi():
|
|
"""Return OpenAPI specification"""
|
|
return send_from_directory('.', 'openapi.json')
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=5005, threaded=True, debug=True)
|