Spaces:
Running
Running
Commit
Β·
2d3b132
0
Parent(s):
first commit
Browse files- README.md +195 -0
- agent_gradio_chat.py +466 -0
- requirements.txt +5 -0
README.md
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# π° AI News Research Assistant
|
2 |
+
|
3 |
+
An intelligent AI-powered news research assistant built with Gradio that provides real-time news search, article fetching, and comprehensive summaries using GPT-OSS models.
|
4 |
+
|
5 |
+
## β¨ Features
|
6 |
+
|
7 |
+
- **Real-time News Search**: Access current headlines from Google News RSS feeds
|
8 |
+
- **Topic-specific Research**: Search for news on specific subjects, companies, or events
|
9 |
+
- **Site-restricted Search**: Limit searches to specific news domains
|
10 |
+
- **Article Content Extraction**: Download and analyze full article content when needed
|
11 |
+
- **AI-powered Summaries**: Get intelligent news summaries with proper citations
|
12 |
+
- **Multiple Model Support**: Choose between GPT-OSS 120B and 20B models
|
13 |
+
- **Modern Web Interface**: Clean, responsive Gradio-based chat interface
|
14 |
+
- **Langfuse Integration**: Built-in observability and tracing for interactions
|
15 |
+
|
16 |
+
## π Quick Start
|
17 |
+
|
18 |
+
### Prerequisites
|
19 |
+
|
20 |
+
- Python 3.8+
|
21 |
+
- API keys for:
|
22 |
+
- Hugging Face (HF_TOKEN)
|
23 |
+
- Serper API (SERPER_API_KEY)
|
24 |
+
- Langfuse (optional, for observability)
|
25 |
+
|
26 |
+
### Installation
|
27 |
+
|
28 |
+
1. **Clone the repository**
|
29 |
+
```bash
|
30 |
+
git clone <your-repo-url>
|
31 |
+
cd agent_gradio_app
|
32 |
+
```
|
33 |
+
|
34 |
+
2. **Install dependencies**
|
35 |
+
```bash
|
36 |
+
pip install -r requirements.txt
|
37 |
+
```
|
38 |
+
|
39 |
+
3. **Set up environment variables**
|
40 |
+
```bash
|
41 |
+
# Create .env file
|
42 |
+
echo "HF_TOKEN=your_huggingface_token" > .env
|
43 |
+
echo "SERPER_API_KEY=your_serper_api_key" >> .env
|
44 |
+
echo "LANGFUSE_PUBLIC_KEY=your_langfuse_public_key" >> .env
|
45 |
+
echo "LANGFUSE_SECRET_KEY=your_langfuse_secret_key" >> .env
|
46 |
+
```
|
47 |
+
|
48 |
+
4. **Run the application**
|
49 |
+
```bash
|
50 |
+
python agent_gradio_chat.py
|
51 |
+
```
|
52 |
+
|
53 |
+
5. **Open your browser**
|
54 |
+
Navigate to `http://localhost:7860`
|
55 |
+
|
56 |
+
## π§ Configuration
|
57 |
+
|
58 |
+
### Available Models
|
59 |
+
|
60 |
+
- **GPT-OSS 120B**: Larger, more capable model for complex reasoning tasks
|
61 |
+
- **GPT-OSS 20B**: Faster, more efficient model for quick responses
|
62 |
+
|
63 |
+
### Environment Variables
|
64 |
+
|
65 |
+
| Variable | Description | Required |
|
66 |
+
|----------|-------------|----------|
|
67 |
+
| `HF_TOKEN` | Hugging Face API token | Yes |
|
68 |
+
| `SERPER_API_KEY` | Serper API key for web search | Yes |
|
69 |
+
| `LANGFUSE_PUBLIC_KEY` | Langfuse public key for observability | No |
|
70 |
+
| `LANGFUSE_SECRET_KEY` | Langfuse secret key for observability | No |
|
71 |
+
|
72 |
+
## π‘ Usage Examples
|
73 |
+
|
74 |
+
### General News Requests
|
75 |
+
- "What are the top news stories today?"
|
76 |
+
- "What's happening in the world right now?"
|
77 |
+
- "Show me the latest headlines"
|
78 |
+
|
79 |
+
### Topic-specific Research
|
80 |
+
- "What's the latest on artificial intelligence?"
|
81 |
+
- "Tell me about recent developments in climate change"
|
82 |
+
- "What's happening with Tesla stock?"
|
83 |
+
|
84 |
+
### Site-specific Searches
|
85 |
+
- "What's the latest climate change news on the BBC?"
|
86 |
+
- "Show me recent AI articles from MIT Technology Review"
|
87 |
+
- "What's new on Ars Technica about cybersecurity?"
|
88 |
+
|
89 |
+
## π οΈ How It Works
|
90 |
+
|
91 |
+
The application uses a sophisticated agent loop that:
|
92 |
+
|
93 |
+
1. **Analyzes User Queries**: Understands the type of news request
|
94 |
+
2. **Selects Appropriate Tools**: Chooses from available search and fetch tools
|
95 |
+
3. **Executes Searches**: Performs targeted news searches using various APIs
|
96 |
+
4. **Extracts Content**: Downloads and processes article content when needed
|
97 |
+
5. **Synthesizes Information**: Provides comprehensive summaries with citations
|
98 |
+
6. **Tracks Interactions**: Logs all interactions for observability
|
99 |
+
|
100 |
+
### Available Tools
|
101 |
+
|
102 |
+
- **`fetch_google_news_rss`**: Get top headlines from Google News
|
103 |
+
- **`serper_news_search`**: Search for specific topics in Google News
|
104 |
+
- **`serper_site_search`**: Restrict searches to specific domains
|
105 |
+
- **`fetch_article`**: Download and extract article content
|
106 |
+
|
107 |
+
## π Project Structure
|
108 |
+
|
109 |
+
```
|
110 |
+
agent_gradio_app/
|
111 |
+
βββ agent_gradio_chat.py # Main application file
|
112 |
+
βββ requirements.txt # Python dependencies
|
113 |
+
βββ config.json # Configuration file
|
114 |
+
βββ .env # Environment variables (create this)
|
115 |
+
βββ README.md # This file
|
116 |
+
βββ run_app.sh # Convenience script to run the app
|
117 |
+
```
|
118 |
+
|
119 |
+
## π API Dependencies
|
120 |
+
|
121 |
+
- **Hugging Face**: Model inference and hosting
|
122 |
+
- **Serper API**: Web search capabilities
|
123 |
+
- **Trafilatura**: Article content extraction
|
124 |
+
- **Langfuse**: Observability and tracing
|
125 |
+
|
126 |
+
## π Deployment
|
127 |
+
|
128 |
+
### Local Development
|
129 |
+
```bash
|
130 |
+
python agent_gradio_chat.py
|
131 |
+
```
|
132 |
+
|
133 |
+
### Production Deployment
|
134 |
+
```bash
|
135 |
+
# Using the convenience script
|
136 |
+
chmod +x run_app.sh
|
137 |
+
./run_app.sh
|
138 |
+
|
139 |
+
# Or directly with custom settings
|
140 |
+
python agent_gradio_chat.py --server-name 0.0.0.0 --server-port 7860
|
141 |
+
```
|
142 |
+
|
143 |
+
### Docker (Optional)
|
144 |
+
```dockerfile
|
145 |
+
FROM python:3.9-slim
|
146 |
+
WORKDIR /app
|
147 |
+
COPY requirements.txt .
|
148 |
+
RUN pip install -r requirements.txt
|
149 |
+
COPY . .
|
150 |
+
EXPOSE 7860
|
151 |
+
CMD ["python", "agent_gradio_chat.py"]
|
152 |
+
```
|
153 |
+
|
154 |
+
## π§ͺ Testing
|
155 |
+
|
156 |
+
Test the application with various news queries:
|
157 |
+
|
158 |
+
```bash
|
159 |
+
# Test basic functionality
|
160 |
+
python -c "
|
161 |
+
from agent_gradio_chat import run_agent
|
162 |
+
response = run_agent('What are the top news stories today?')
|
163 |
+
print(response)
|
164 |
+
"
|
165 |
+
```
|
166 |
+
|
167 |
+
## π€ Contributing
|
168 |
+
|
169 |
+
1. Fork the repository
|
170 |
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
171 |
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
172 |
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
173 |
+
5. Open a Pull Request
|
174 |
+
|
175 |
+
## π License
|
176 |
+
|
177 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
178 |
+
|
179 |
+
## π Acknowledgments
|
180 |
+
|
181 |
+
- [Gradio](https://gradio.app/) for the web interface framework
|
182 |
+
- [Hugging Face](https://huggingface.co/) for model hosting and inference
|
183 |
+
- [OpenAI](https://openai.com/) for the GPT-OSS models
|
184 |
+
- [Serper](https://serper.dev/) for web search capabilities
|
185 |
+
|
186 |
+
## π Support
|
187 |
+
|
188 |
+
For issues, questions, or contributions:
|
189 |
+
- Open an issue on GitHub
|
190 |
+
- Check the documentation
|
191 |
+
- Review the code comments for implementation details
|
192 |
+
|
193 |
+
---
|
194 |
+
|
195 |
+
**Happy news researching! ππ°**
|
agent_gradio_chat.py
ADDED
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import time
|
5 |
+
import requests
|
6 |
+
import trafilatura
|
7 |
+
import xml.etree.ElementTree as ET
|
8 |
+
from typing import Any, Dict, List, Optional
|
9 |
+
from openai import OpenAI
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from langfuse import Langfuse
|
12 |
+
|
13 |
+
load_dotenv()
|
14 |
+
|
15 |
+
# ---------- Config ----------
|
16 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
17 |
+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
|
18 |
+
assert HF_TOKEN, "Missing HF_TOKEN"
|
19 |
+
assert SERPER_API_KEY, "Missing SERPER_API_KEY"
|
20 |
+
|
21 |
+
# Available models for selection
|
22 |
+
AVAILABLE_MODELS = [
|
23 |
+
"openai/gpt-oss-120b:fireworks-ai",
|
24 |
+
"openai/gpt-oss-20b:fireworks-ai"
|
25 |
+
]
|
26 |
+
|
27 |
+
# Default model
|
28 |
+
DEFAULT_MODEL = "openai/gpt-oss-120b:fireworks-ai"
|
29 |
+
BASE_URL = "https://router.huggingface.co/v1"
|
30 |
+
|
31 |
+
client = OpenAI(base_url=BASE_URL, api_key=HF_TOKEN)
|
32 |
+
|
33 |
+
langfuse = Langfuse(
|
34 |
+
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
|
35 |
+
secret_key=os.getenv("LANGFUSE_SECRET_KEY")
|
36 |
+
)
|
37 |
+
|
38 |
+
# ---------- Tools ----------
|
39 |
+
def fetch_google_news_rss(num: int = 10) -> List[Dict[str, Any]]:
|
40 |
+
"""Fetch general news from Google News RSS feed."""
|
41 |
+
try:
|
42 |
+
url = "https://news.google.com/rss"
|
43 |
+
r = requests.get(url, timeout=30)
|
44 |
+
r.raise_for_status()
|
45 |
+
|
46 |
+
# Parse RSS XML
|
47 |
+
root = ET.fromstring(r.content)
|
48 |
+
items = root.findall('.//item')
|
49 |
+
|
50 |
+
results = []
|
51 |
+
for item in items[:num]:
|
52 |
+
title = item.find('title')
|
53 |
+
link = item.find('link')
|
54 |
+
pub_date = item.find('pubDate')
|
55 |
+
source = item.find('source')
|
56 |
+
|
57 |
+
results.append({
|
58 |
+
"title": title.text if title is not None else "No title",
|
59 |
+
"link": link.text if link is not None else "",
|
60 |
+
"pub_date": pub_date.text if pub_date is not None else "No date",
|
61 |
+
"source": source.text if source is not None else "Google News"
|
62 |
+
})
|
63 |
+
|
64 |
+
return results
|
65 |
+
except Exception as e:
|
66 |
+
return {"ok": False, "error": repr(e)}
|
67 |
+
|
68 |
+
def serper_news_search(query: str, num: int = 5) -> List[Dict[str, Any]]:
|
69 |
+
"""Fetch news for a specific topic or query."""
|
70 |
+
url = "https://google.serper.dev/news"
|
71 |
+
headers = {"X-API-KEY": SERPER_API_KEY, "Content-Type": "application/json"}
|
72 |
+
payload = {"q": query, "gl": "us", "hl": "en", "tbs": "qdr:d"}
|
73 |
+
r = requests.post(url, headers=headers, json=payload, timeout=30)
|
74 |
+
r.raise_for_status()
|
75 |
+
data = r.json()
|
76 |
+
results = []
|
77 |
+
for item in data.get("news", [])[:num]:
|
78 |
+
results.append({
|
79 |
+
"title": item.get("title"),
|
80 |
+
"link": item.get("link"),
|
81 |
+
"snippet": item.get("snippet"),
|
82 |
+
"date": item.get("date"), # ISO8601 when available
|
83 |
+
"source": item.get("source")
|
84 |
+
})
|
85 |
+
return results
|
86 |
+
|
87 |
+
def serper_site_search(query: str, site: str, num: int = 5) -> List[Dict[str, Any]]:
|
88 |
+
"""Site restricted web search."""
|
89 |
+
url = "https://google.serper.dev/search"
|
90 |
+
headers = {"X-API-KEY": SERPER_API_KEY, "Content-Type": "application/json"}
|
91 |
+
payload = {"q": f"site:{site} {query}", "gl": "us", "hl": "en"}
|
92 |
+
r = requests.post(url, headers=headers, json=payload, timeout=30)
|
93 |
+
r.raise_for_status()
|
94 |
+
data = r.json()
|
95 |
+
results = []
|
96 |
+
for item in data.get("organic", [])[:num]:
|
97 |
+
results.append({
|
98 |
+
"title": item.get("title"),
|
99 |
+
"link": item.get("link"),
|
100 |
+
"snippet": item.get("snippet"),
|
101 |
+
"favicons": item.get("favicons", {})
|
102 |
+
})
|
103 |
+
return results
|
104 |
+
|
105 |
+
def fetch_article(url: str, max_chars: int = 12000) -> Dict[str, Any]:
|
106 |
+
"""Fetch and extract clean article text with trafilatura."""
|
107 |
+
try:
|
108 |
+
downloaded = trafilatura.fetch_url(url, timeout=30)
|
109 |
+
text = trafilatura.extract(downloaded, include_comments=False) if downloaded else None
|
110 |
+
if not text:
|
111 |
+
return {"ok": False, "error": "could_not_extract"}
|
112 |
+
text = text.strip()
|
113 |
+
if len(text) > max_chars:
|
114 |
+
text = text[:max_chars] + " ..."
|
115 |
+
return {"ok": True, "text": text}
|
116 |
+
except Exception as e:
|
117 |
+
return {"ok": False, "error": repr(e)}
|
118 |
+
|
119 |
+
# OpenAI-style tool specs for function calling
|
120 |
+
TOOLS = [
|
121 |
+
{
|
122 |
+
"type": "function",
|
123 |
+
"function": {
|
124 |
+
"name": "fetch_google_news_rss",
|
125 |
+
"description": "Fetch general top headlines from Google News RSS feed. Use this when you want to see what's happening in the world today without a specific topic focus.",
|
126 |
+
"parameters": {
|
127 |
+
"type": "object",
|
128 |
+
"properties": {
|
129 |
+
"num": {"type": "integer", "minimum": 1, "maximum": 20, "description": "Number of news items to fetch"}
|
130 |
+
},
|
131 |
+
"required": []
|
132 |
+
}
|
133 |
+
}
|
134 |
+
},
|
135 |
+
{
|
136 |
+
"type": "function",
|
137 |
+
"function": {
|
138 |
+
"name": "serper_news_search",
|
139 |
+
"description": "Search Google News for articles about a specific topic or query. Use this when you need news about particular subjects, companies, or events.",
|
140 |
+
"parameters": {
|
141 |
+
"type": "object",
|
142 |
+
"properties": {
|
143 |
+
"query": {"type": "string"},
|
144 |
+
"num": {"type": "integer", "minimum": 1, "maximum": 20}
|
145 |
+
},
|
146 |
+
"required": ["query"]
|
147 |
+
}
|
148 |
+
}
|
149 |
+
},
|
150 |
+
{
|
151 |
+
"type": "function",
|
152 |
+
"function": {
|
153 |
+
"name": "serper_site_search",
|
154 |
+
"description": "Search a specific news domain for relevant articles.",
|
155 |
+
"parameters": {
|
156 |
+
"type": "object",
|
157 |
+
"properties": {
|
158 |
+
"query": {"type": "string"},
|
159 |
+
"site": {"type": "string", "description": "Domain like ft.com or nytimes.com"},
|
160 |
+
"num": {"type": "integer", "minimum": 1, "maximum": 10}
|
161 |
+
},
|
162 |
+
"required": ["query", "site"]
|
163 |
+
}
|
164 |
+
}
|
165 |
+
},
|
166 |
+
{
|
167 |
+
"type": "function",
|
168 |
+
"function": {
|
169 |
+
"name": "fetch_article",
|
170 |
+
"description": "Download and extract the main text of an article from a URL. ONLY use this when the user asks specific questions about article content, details, or wants to analyze/quote from particular articles. Do NOT use this for general news summaries or overviews.",
|
171 |
+
"parameters": {
|
172 |
+
"type": "object",
|
173 |
+
"properties": {
|
174 |
+
"url": {"type": "string"},
|
175 |
+
"max_chars": {"type": "integer", "minimum": 1000, "maximum": 60000}
|
176 |
+
},
|
177 |
+
"required": ["url"]
|
178 |
+
}
|
179 |
+
}
|
180 |
+
}
|
181 |
+
]
|
182 |
+
|
183 |
+
FUNCTION_MAP = {
|
184 |
+
"fetch_google_news_rss": fetch_google_news_rss,
|
185 |
+
"serper_news_search": serper_news_search,
|
186 |
+
"serper_site_search": serper_site_search,
|
187 |
+
"fetch_article": fetch_article,
|
188 |
+
}
|
189 |
+
|
190 |
+
# ---------- Agent loop ----------
|
191 |
+
def call_model(messages: List[Dict[str, str]], tools=TOOLS, temperature: float = 0.3, model: str = DEFAULT_MODEL):
|
192 |
+
"""One step with tool calling support."""
|
193 |
+
try:
|
194 |
+
return client.chat.completions.create(
|
195 |
+
model=model,
|
196 |
+
temperature=temperature,
|
197 |
+
messages=messages,
|
198 |
+
tools=tools,
|
199 |
+
tool_choice="auto"
|
200 |
+
)
|
201 |
+
except Exception as e:
|
202 |
+
print(f"Error calling model: {e}")
|
203 |
+
raise
|
204 |
+
|
205 |
+
def run_agent(user_prompt: str, site_limit: Optional[str] = None, model: str = DEFAULT_MODEL) -> str:
|
206 |
+
"""
|
207 |
+
High level prompt for a news agent.
|
208 |
+
It may search, read links, then synthesize and cite URLs.
|
209 |
+
"""
|
210 |
+
system = {
|
211 |
+
"role": "system",
|
212 |
+
"content": (
|
213 |
+
"You are a careful news agent. Follow these steps:\n"
|
214 |
+
"1. For general news requests: Use fetch_google_news_rss to get top headlines\n"
|
215 |
+
"2. For specific topic requests: Use serper_news_search with the topic\n"
|
216 |
+
"3. ONLY use fetch_article when the user asks specific questions about article content, details, or wants to analyze/quote from particular articles\n"
|
217 |
+
"4. For general news summaries, provide information based on headlines and snippets without fetching full articles\n"
|
218 |
+
"5. STOP calling tools and provide your final answer\n"
|
219 |
+
"6. Always include a bullet list of sources with URLs\n"
|
220 |
+
"IMPORTANT: After reading articles (if any), you must provide your final answer without calling more tools.\n\n"
|
221 |
+
"TOOL SELECTION GUIDE:\n"
|
222 |
+
"- fetch_google_news_rss: Use for 'what's happening today' or 'top news' requests\n"
|
223 |
+
"- serper_news_search: Use for specific topics like 'AI chips', 'Nvidia', 'climate change'\n"
|
224 |
+
"- serper_site_search: Use when restricted to specific news sources\n"
|
225 |
+
"- fetch_article: ONLY use when user asks about specific article content, details, or wants to analyze particular articles\n"
|
226 |
+
"PRIORITY: For general news requests, provide summaries based on headlines and snippets. Only fetch full articles when specifically needed for detailed analysis.\n"
|
227 |
+
),
|
228 |
+
}
|
229 |
+
|
230 |
+
messages: List[Dict[str, str]] = [system, {"role": "user", "content": user_prompt}]
|
231 |
+
if site_limit:
|
232 |
+
messages.append({"role": "user", "content": f"Restrict searches to {site_limit} when appropriate."})
|
233 |
+
|
234 |
+
for step in range(6): # small safety cap
|
235 |
+
try:
|
236 |
+
resp = call_model(messages, model=model)
|
237 |
+
msg = resp.choices[0].message
|
238 |
+
|
239 |
+
# If the model wants to call tools
|
240 |
+
if getattr(msg, "tool_calls", None) and msg.tool_calls:
|
241 |
+
# Add the assistant message with tool calls to the conversation
|
242 |
+
assistant_message = {
|
243 |
+
"role": "assistant",
|
244 |
+
"content": msg.content or "",
|
245 |
+
"tool_calls": [
|
246 |
+
{
|
247 |
+
"id": tool_call.id,
|
248 |
+
"type": "function",
|
249 |
+
"function": {
|
250 |
+
"name": tool_call.function.name,
|
251 |
+
"arguments": tool_call.function.arguments
|
252 |
+
}
|
253 |
+
}
|
254 |
+
for tool_call in msg.tool_calls
|
255 |
+
]
|
256 |
+
}
|
257 |
+
messages.append(assistant_message)
|
258 |
+
|
259 |
+
# Process each tool call
|
260 |
+
for tool_call in msg.tool_calls:
|
261 |
+
name = tool_call.function.name
|
262 |
+
args = {}
|
263 |
+
try:
|
264 |
+
args = json.loads(tool_call.function.arguments or "{}")
|
265 |
+
except json.JSONDecodeError:
|
266 |
+
args = {}
|
267 |
+
|
268 |
+
fn = FUNCTION_MAP.get(name)
|
269 |
+
if not fn:
|
270 |
+
messages.append({
|
271 |
+
"role": "tool",
|
272 |
+
"tool_call_id": tool_call.id,
|
273 |
+
"name": name,
|
274 |
+
"content": json.dumps({"ok": False, "error": "unknown_tool"})
|
275 |
+
})
|
276 |
+
continue
|
277 |
+
|
278 |
+
try:
|
279 |
+
result = fn(**args)
|
280 |
+
except TypeError as e:
|
281 |
+
result = {"ok": False, "error": f"bad_args: {e}"}
|
282 |
+
except Exception as e:
|
283 |
+
result = {"ok": False, "error": repr(e)}
|
284 |
+
|
285 |
+
tool_response = {
|
286 |
+
"role": "tool",
|
287 |
+
"tool_call_id": tool_call.id,
|
288 |
+
"name": name,
|
289 |
+
"content": json.dumps(result),
|
290 |
+
}
|
291 |
+
messages.append(tool_response)
|
292 |
+
|
293 |
+
# After processing tools, add a reminder to synthesize
|
294 |
+
if step >= 2: # After 2+ tool calls, encourage synthesis
|
295 |
+
messages.append({
|
296 |
+
"role": "user",
|
297 |
+
"content": "You now have sufficient information. Please provide your final answer with sources."
|
298 |
+
})
|
299 |
+
|
300 |
+
# Continue loop so the model can see tool outputs
|
301 |
+
continue
|
302 |
+
|
303 |
+
# If we have a final assistant message without tool calls
|
304 |
+
if msg.content:
|
305 |
+
return msg.content
|
306 |
+
|
307 |
+
# Fallback tiny sleep then continue
|
308 |
+
time.sleep(0.2)
|
309 |
+
|
310 |
+
except Exception as e:
|
311 |
+
# If there's an error, try to continue or return error message
|
312 |
+
if step == 5: # Last step
|
313 |
+
return f"Error occurred during processing: {e}"
|
314 |
+
time.sleep(0.5)
|
315 |
+
continue
|
316 |
+
|
317 |
+
return "I could not complete the task within the step limit. Try refining your query."
|
318 |
+
|
319 |
+
# ---------- Gradio Interface ----------
|
320 |
+
def chat_with_agent(message, history, model):
|
321 |
+
"""Handle chat messages and return agent responses."""
|
322 |
+
if not message.strip():
|
323 |
+
return history
|
324 |
+
|
325 |
+
# Create a trace for this interaction
|
326 |
+
trace = langfuse.trace(
|
327 |
+
name="chat_interaction",
|
328 |
+
input={"user_message": message, "model": model, "history_length": len(history)}
|
329 |
+
)
|
330 |
+
|
331 |
+
try:
|
332 |
+
response = run_agent(message, None, model)
|
333 |
+
|
334 |
+
# Update trace with success info and output
|
335 |
+
trace.update(
|
336 |
+
output={"agent_response": response},
|
337 |
+
metadata={
|
338 |
+
"model": model,
|
339 |
+
"message_length": len(message),
|
340 |
+
"response_length": len(response),
|
341 |
+
"success": True
|
342 |
+
}
|
343 |
+
)
|
344 |
+
|
345 |
+
# Flush the trace to send it to Langfuse
|
346 |
+
langfuse.flush()
|
347 |
+
|
348 |
+
history.append({"role": "user", "content": message})
|
349 |
+
history.append({"role": "assistant", "content": response})
|
350 |
+
return history
|
351 |
+
|
352 |
+
except Exception as e:
|
353 |
+
# Update trace with error info
|
354 |
+
trace.update(
|
355 |
+
output={"error": str(e)},
|
356 |
+
level="ERROR",
|
357 |
+
metadata={
|
358 |
+
"error": str(e),
|
359 |
+
"success": False
|
360 |
+
}
|
361 |
+
)
|
362 |
+
|
363 |
+
# Flush the trace to send it to Langfuse
|
364 |
+
langfuse.flush()
|
365 |
+
|
366 |
+
error_msg = f"Sorry, I encountered an error: {str(e)}"
|
367 |
+
history.append({"role": "user", "content": message})
|
368 |
+
history.append({"role": "assistant", "content": error_msg})
|
369 |
+
return history
|
370 |
+
|
371 |
+
def clear_chat():
|
372 |
+
"""Clear the chat history."""
|
373 |
+
return [], ""
|
374 |
+
|
375 |
+
# Create the Gradio interface
|
376 |
+
with gr.Blocks(
|
377 |
+
title="Chat with the News",
|
378 |
+
theme=gr.themes.Monochrome()
|
379 |
+
) as demo:
|
380 |
+
|
381 |
+
# Header using Gradio markdown
|
382 |
+
gr.Markdown("""
|
383 |
+
# π° Chat with the News
|
384 |
+
|
385 |
+
Your AI-powered news research assistant with real-time search capabilities, based on [GPT-OSS models](https://huggingface.co/collections/openai/gpt-oss-68911959590a1634ba11c7a4) and running on inference providers.
|
386 |
+
""")
|
387 |
+
|
388 |
+
# Examples section using Gradio markdown
|
389 |
+
gr.Markdown("""
|
390 |
+
### π‘ Try these examples:
|
391 |
+
|
392 |
+
- **General:** "What are the top news stories today?"
|
393 |
+
- **Specific topic:** "What's the latest on artificial intelligence?"
|
394 |
+
- **Site-specific:** "What's the latest climate change news on the BBC?"
|
395 |
+
""")
|
396 |
+
|
397 |
+
# Model selector
|
398 |
+
model_selector = gr.Dropdown(
|
399 |
+
choices=AVAILABLE_MODELS,
|
400 |
+
value=DEFAULT_MODEL,
|
401 |
+
label="π€ Select Model",
|
402 |
+
info="Choose between GPT-OSS 120B and 20B models"
|
403 |
+
)
|
404 |
+
|
405 |
+
# Message input
|
406 |
+
msg = gr.Textbox(
|
407 |
+
label="Ask me about the news",
|
408 |
+
placeholder="What would you like to know about today?",
|
409 |
+
lines=2
|
410 |
+
)
|
411 |
+
|
412 |
+
# Buttons in a row
|
413 |
+
with gr.Row():
|
414 |
+
submit_btn = gr.Button("π Send", variant="primary", size="lg")
|
415 |
+
clear_btn = gr.Button("ποΈ Clear Chat", variant="secondary", size="lg")
|
416 |
+
|
417 |
+
# Chat interface
|
418 |
+
chatbot = gr.Chatbot(
|
419 |
+
label="News Agent",
|
420 |
+
height=500,
|
421 |
+
show_label=False,
|
422 |
+
container=True,
|
423 |
+
type="messages"
|
424 |
+
)
|
425 |
+
|
426 |
+
# Event handlers
|
427 |
+
submit_btn.click(
|
428 |
+
chat_with_agent,
|
429 |
+
inputs=[msg, chatbot, model_selector],
|
430 |
+
outputs=[chatbot],
|
431 |
+
show_progress=True
|
432 |
+
)
|
433 |
+
|
434 |
+
msg.submit(
|
435 |
+
chat_with_agent,
|
436 |
+
inputs=[msg, chatbot, model_selector],
|
437 |
+
outputs=[chatbot],
|
438 |
+
show_progress=True
|
439 |
+
)
|
440 |
+
|
441 |
+
clear_btn.click(
|
442 |
+
clear_chat,
|
443 |
+
outputs=[chatbot, msg]
|
444 |
+
)
|
445 |
+
|
446 |
+
# Instructions using Gradio markdown
|
447 |
+
gr.Markdown("""
|
448 |
+
---
|
449 |
+
|
450 |
+
### βΉοΈ How it works
|
451 |
+
|
452 |
+
This AI agent can search Google News, fetch articles from specific sources, and provide comprehensive news summaries with proper citations. It uses real-time data and can restrict searches to specific news domains when requested.
|
453 |
+
|
454 |
+
**Model Selection:**
|
455 |
+
- **GPT-OSS 120B**: Larger, more capable model for complex reasoning tasks
|
456 |
+
- **GPT-OSS 20B**: Faster, more efficient model for quick responses
|
457 |
+
""")
|
458 |
+
|
459 |
+
# Launch the app
|
460 |
+
if __name__ == "__main__":
|
461 |
+
demo.launch(
|
462 |
+
server_name="0.0.0.0",
|
463 |
+
server_port=7860,
|
464 |
+
share=False,
|
465 |
+
show_error=True
|
466 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
openai
|
3 |
+
python-dotenv
|
4 |
+
trafilatura
|
5 |
+
langfuse
|