hermes-agent / main.py
D3AMON's picture
Upload main.py with huggingface_hub
bcbf94e verified
Raw History Blame Contribute Delete
18.7 kB
"""
Hermes Agent - FastAPI Backend Server
Serves both the API endpoints and Telegram webhook
"""
import os
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import config
from hermes_agent import process_query, get_agent
from telegram_bot import get_bot, TelegramBot
# Configure logging
logging.basicConfig(
level=getattr(logging, config.LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("hermes-api")
# Check if we have the API key set
try:
config.validate_config()
logger.info("Configuration validated successfully")
except ValueError as e:
logger.warning(f"Configuration issue: {e}")
# App metadata for documentation
APP_METADATA = {
"title": "Hermes Agent API",
"description": """
๐Ÿค– Hermes Agent - AI-powered assistant with 100+ API integrations
Built with Nous Research Hermes function calling standard.
## Features
- **Weather** - Global forecasts for any city
- **Crypto** - Real-time prices for 1000+ coins
- **Anime** - Search MyAnimeList database
- **Games** - Pokemon, D&D, chess data
- **Food** - 300,000+ recipes and cocktails
- **Space** - ISS tracking, SpaceX launches, NASA APOD
- **Sports** - Football, NBA, F1 standings
- **Quotes** - Inspirational, movie, TV quotes
- **Science** - Periodic table, number facts
- **News** - Hacker News, space news
- **Entertainment** - Jokes, memes, trivia
- **Utility** - Translation, currency conversion, QR codes
## Usage
Send a POST request to `/chat` with your message and get an AI response
that intelligently uses tools when needed.
""",
"version": "1.0.0",
"contact": {
"name": "Hermes Agent",
"url": "https://github.com/NousResearch/Hermes-Function-Calling"
}
}
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan context manager for startup/shutdown events"""
# Startup
logger.info("๐Ÿš€ Hermes Agent starting up...")
logger.info(f"Model: {config.DEFAULT_MODEL}")
logger.info(f"Max recursion depth: {config.MAX_RECURSION_DEPTH}")
# Initialize agent
agent = get_agent()
logger.info("โœ… Agent initialized")
# Setup Telegram bot in polling mode for maximum reliability on HF
if config.TELEGRAM_BOT_TOKEN:
try:
bot = get_bot()
# Run polling in a separate thread to not block FastAPI
import threading
def start_polling():
bot.run_polling()
polling_thread = threading.Thread(target=start_polling, daemon=True)
polling_thread.start()
logger.info("โœ… Telegram bot started in polling mode (background thread)")
except Exception as e:
logger.warning(f"โš ๏ธ Could not start Telegram bot: {e}")
yield
# Shutdown
logger.info("๐Ÿ›‘ Hermes Agent shutting down...")
# Create FastAPI app
app = FastAPI(
title=APP_METADATA["title"],
description=APP_METADATA["description"],
version=APP_METADATA["version"],
contact=APP_METADATA["contact"],
lifespan=lifespan
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ============================================================
# HTML DASHBOARD (Landing Page)
# ============================================================
DASHBOARD_HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermes Agent - AI with 100+ APIs</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e94560;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
header {
text-align: center;
padding: 60px 20px;
}
h1 {
font-size: 3.5em;
margin-bottom: 10px;
background: linear-gradient(135deg, #e94560 0%, #ff6b6b 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(233, 69, 96, 0.3);
}
.subtitle {
font-size: 1.3em;
color: #a0a0a0;
margin-bottom: 30px;
}
.status {
display: inline-block;
padding: 10px 25px;
background: rgba(0, 255, 100, 0.1);
border: 1px solid rgba(0, 255, 100, 0.3);
border-radius: 50px;
color: #00ff64;
font-weight: bold;
}
.status.online::before {
content: "โ— ";
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
margin-top: 50px;
}
.card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 30px;
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(233, 69, 96, 0.3);
}
.card h3 {
font-size: 1.3em;
margin-bottom: 15px;
color: #e94560;
}
.card p {
color: #b0b0b0;
line-height: 1.6;
font-size: 0.95em;
}
.card .icon {
font-size: 2em;
margin-bottom: 10px;
}
.endpoints {
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px;
margin-top: 50px;
}
.endpoints h2 {
text-align: center;
margin-bottom: 20px;
color: #e94560;
}
.endpoint {
display: flex;
align-items: center;
padding: 15px;
margin: 10px 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
font-family: monospace;
}
.method {
padding: 5px 15px;
border-radius: 5px;
margin-right: 15px;
font-weight: bold;
font-size: 0.85em;
}
.method.get { background: #4CAF50; color: white; }
.method.post { background: #2196F3; color: white; }
.path {
color: #fff;
}
.footer {
text-align: center;
margin-top: 50px;
padding: 30px;
color: #666;
}
.footer a {
color: #e94560;
text-decoration: none;
}
.stats {
display: flex;
justify-content: center;
gap: 50px;
margin: 40px 0;
flex-wrap: wrap;
}
.stat {
text-align: center;
}
.stat .number {
font-size: 2.5em;
font-weight: bold;
color: #e94560;
}
.stat .label {
color: #888;
font-size: 0.9em;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Hermes Agent</h1>
<p class="subtitle">AI-Powered Assistant with 100+ Real-World API Integrations</p>
<div class="status online">Online</div>
<div class="stats">
<div class="stat">
<div class="number">100+</div>
<div class="label">APIs Integrated</div>
</div>
<div class="stat">
<div class="number">50+</div>
<div class="label">Categories</div>
</div>
<div class="stat">
<div class="number">โˆž</div>
<div class="label">Possibilities</div>
</div>
</div>
</header>
<div class="grid">
<div class="card">
<div class="icon">๐ŸŒค๏ธ</div>
<h3>Weather & Environment</h3>
<p>Global weather forecasts, air quality, UV index, and environmental data for any location on Earth.</p>
</div>
<div class="card">
<div class="icon">๐Ÿ’ฐ</div>
<h3>Crypto & Finance</h3>
<p>Real-time cryptocurrency prices, market data, exchange rates, and financial information.</p>
</div>
<div class="card">
<div class="icon">๐ŸŽฎ</div>
<h3>Games & Entertainment</h3>
<p>Pokemon database, D&D references, anime search, game stats, and entertainment data.</p>
</div>
<div class="card">
<div class="icon">๐Ÿš€</div>
<h3>Space & Science</h3>
<p>ISS tracking, SpaceX launches, NASA imagery, periodic table, and scientific facts.</p>
</div>
<div class="card">
<div class="icon">๐Ÿ•</div>
<h3>Food & Drink</h3>
<p>300,000+ recipes, cocktail instructions, brewery database, and food information.</p>
</div>
<div class="card">
<div class="icon">โšฝ</div>
<h3>Sports</h3>
<p>Football standings, NBA games, F1 results, chess ratings, and bike sharing data.</p>
</div>
<div class="card">
<div class="icon">๐Ÿ“š</div>
<h3>Books & Knowledge</h3>
<p>Book search, dictionary definitions, quotes, trivia questions, and educational content.</p>
</div>
<div class="card">
<div class="icon">๐Ÿ˜‚</div>
<h3>Fun & Social</h3>
<p>Jokes, memes, Reddit posts, Chuck Norris facts, Rick & Morty data, and more.</p>
</div>
</div>
<div class="endpoints">
<h2>๐Ÿ”— API Endpoints</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/ - Dashboard (this page)</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/health - Health check</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span class="path">/chat - Send a message to Hermes</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span class="path">/telegram/webhook - Telegram bot webhook</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/tools - List all available tools</span>
</div>
</div>
<div class="footer">
<p>Powered by <a href="https://nousresearch.com">Nous Research Hermes</a> |
Built with <a href="https://fastapi.tiangolo.com">FastAPI</a> |
Deployed on <a href="https://huggingface.co/spaces">Hugging Face Spaces</a></p>
<p style="margin-top: 10px;">Based on <a href="https://github.com/NousResearch/Hermes-Function-Calling">Hermes-Function-Calling</a></p>
</div>
</div>
</body>
</html>
"""
# ============================================================
# API ENDPOINTS
# ============================================================
@app.get("/", response_class=HTMLResponse)
async def dashboard():
"""Serve the dashboard HTML page"""
return HTMLResponse(content=DASHBOARD_HTML)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"version": "1.0.0",
"model": config.DEFAULT_MODEL,
"features": {
"agent": True,
"telegram": bool(config.TELEGRAM_BOT_TOKEN),
"tools_count": 100,
"api_categories": 50
}
}
@app.post("/chat")
async def chat(request: Request):
"""
Main chat endpoint - send a message to Hermes Agent
Request body:
{
"message": "Your question here",
"user_id": "optional_user_id"
}
"""
try:
data = await request.json()
message = data.get("message", "")
user_id = data.get("user_id", "default")
if not message:
raise HTTPException(status_code=400, detail="Message is required")
# Process through Hermes agent
response = process_query(message, user_id)
return {
"response": response,
"user_id": user_id,
"status": "success"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Chat error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/tools")
async def list_tools():
"""List all available tools/functions"""
from api_functions import get_all_function_names, get_function_schemas
return {
"functions": get_all_function_names(),
"count": len(get_all_function_names()),
"schemas": get_function_schemas()
}
@app.post("/telegram/webhook")
async def telegram_webhook(request: Request):
"""
Telegram webhook endpoint
Receives updates from Telegram when the bot is in webhook mode
"""
try:
update_data = await request.json()
bot = get_bot()
bot.process_update(update_data)
return {"status": "ok"}
except Exception as e:
logger.error(f"Webhook error: {e}")
return {"status": "error", "message": str(e)}
@app.get("/telegram/webhook-info")
async def telegram_webhook_info():
"""Get Telegram webhook information"""
try:
bot = get_bot()
info = await bot.application.bot.get_webhook_info()
return info.to_dict()
except Exception as e:
return {"error": str(e)}
@app.get("/demo")
async def demo_page():
"""Interactive demo page"""
return HTMLResponse(content="""
<!DOCTYPE html>
<html>
<head>
<title>Hermes Agent - Demo</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; }
.chat { border: 1px solid #333; border-radius: 10px; padding: 20px; height: 400px; overflow-y: auto; background: rgba(255,255,255,0.05); }
.input-area { display: flex; gap: 10px; margin-top: 20px; }
input { flex: 1; padding: 15px; border-radius: 10px; border: 1px solid #333; background: rgba(255,255,255,0.1); color: #fff; font-size: 16px; }
button { padding: 15px 30px; border-radius: 10px; border: none; background: #e94560; color: white; cursor: pointer; font-size: 16px; }
button:hover { background: #ff6b6b; }
.msg { padding: 10px; margin: 5px 0; border-radius: 10px; }
.user { background: rgba(233, 69, 96, 0.2); text-align: right; }
.bot { background: rgba(255, 255, 255, 0.1); text-align: left; }
.typing { color: #888; font-style: italic; }
</style>
</head>
<body>
<h1>๐Ÿค– Hermes Agent Demo</h1>
<div class="chat" id="chat"></div>
<div class="input-area">
<input type="text" id="message" placeholder="Ask me anything..." onkeypress="if(event.key==='Enter') send()">
<button onclick="send()">Send</button>
</div>
<script>
async function send() {
const input = document.getElementById('message');
const chat = document.getElementById('chat');
const msg = input.value.trim();
if (!msg) return;
chat.innerHTML += '<div class="msg user"><b>You:</b> ' + msg + '</div>';
chat.innerHTML += '<div class="msg bot typing">Hermes is thinking...</div>';
input.value = '';
chat.scrollTop = chat.scrollHeight;
try {
const res = await fetch('/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: msg, user_id: 'demo'})
});
const data = await res.json();
// Remove typing indicator
const typing = chat.querySelector('.typing');
if (typing) typing.remove();
chat.innerHTML += '<div class="msg bot"><b>Hermes:</b> ' + data.response.replace(/\n/g, '<br>') + '</div>';
} catch (e) {
chat.innerHTML += '<div class="msg bot" style="color:#f66;">Error: ' + e.message + '</div>';
}
chat.scrollTop = chat.scrollHeight;
}
</script>
</body>
</html>
""")
# Error handlers
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Handle all unhandled exceptions"""
logger.error(f"Unhandled exception: {exc}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error", "error": str(exc)}
)
# Startup validation
@app.on_event("startup")
async def startup_event():
"""Run on startup"""
logger.info("=" * 60)
logger.info("HERMES AGENT STARTED")
logger.info(f"Port: {config.PORT}")
logger.info(f"Debug: {config.DEBUG}")
logger.info("=" * 60)
if __name__ == "__main__":
# Run the server
uvicorn.run(
"main:app",
host=config.HOST,
port=config.PORT,
reload=config.DEBUG,
log_level="info"
)