-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #71 from ygarg25/develop
added/feature: Portfolio News Agent
- Loading branch information
Showing
11 changed files
with
463 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Benchmarking & Testing News Agent Guide | ||
|
||
|
||
## How to Run the Tests: | ||
1) In the parent directory: | ||
- ```cd submodules/moragents_dockers/agents``` | ||
|
||
2) ```docker build -t agent .``` | ||
|
||
2. NOTE: If you are using Apple Silicon then you may experience problems due to the base image not being build for arm64. We have included a separate Dockerfile in order to deal with this issue, run: | ||
|
||
- ```docker build . -t agent -f Dockerfile-apple``` | ||
|
||
3) To run the agent: | ||
|
||
- ```docker run --name agent -p 5000:5000 agent``` | ||
|
||
4) Check if the agent is up and running on docker or not | ||
5) If it is running, navigate to `submodules/news_agent_benchmarks` | ||
6) run `pytest benchmarks.py` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# submodules/benchmarks/news_agent_benchmarks/benchmarks.py | ||
|
||
import pytest | ||
import logging | ||
from submodules.benchmarks.news_agent_benchmarks.config import Config | ||
from submodules.benchmarks.news_agent_benchmarks.helpers import ask_news_agent, extract_classification | ||
|
||
logging.basicConfig(level=logging.INFO) | ||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def test_news_classification(): | ||
for i, test_case in enumerate(Config.TEST_CASES): | ||
article_text = test_case["article_text"] | ||
expected_classification = test_case["expected_classification"] | ||
|
||
logger.info(f"Testing article classification (Test {i + 1})") | ||
|
||
# Ask the news agent to classify the article | ||
response = ask_news_agent(article_text, Config.LOCAL_AGENT_URL) | ||
|
||
# Extract the classification from the response | ||
classification = extract_classification(response) | ||
|
||
if classification == "UNKNOWN": | ||
logger.warning(f"Test case {i + 1} resulted in UNKNOWN classification. Response: {response}") | ||
assert False, f"Test case {i + 1} failed: Could not determine classification" | ||
else: | ||
# Check if the classification matches the expected classification | ||
assert classification == expected_classification, f"Test case {i + 1} failed: Expected {expected_classification}, but got {classification}" | ||
|
||
logger.info(f"Test case {i + 1} passed: Correctly classified as {classification}") | ||
|
||
logger.info("All test cases passed successfully") | ||
|
||
|
||
if __name__ == "__main__": | ||
pytest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
class Config: | ||
TEST_CASES = [ | ||
{ | ||
"article_text": "ETH Prices zooms 10 percent today while Bitcoin's price surged to $70,000 today, breaking all previous records. Analysts attribute this to increased institutional adoption and positive regulatory news from several countries.", | ||
"expected_classification": "RELEVANT", | ||
}, | ||
{ | ||
"article_text": "A new Tesla facility has opened in Texas, utilizing 100% renewable energy.", | ||
"expected_classification": "NOT RELEVANT", | ||
}, | ||
# Add more test cases as needed | ||
] | ||
|
||
LOCAL_AGENT_URL = "http://127.0.0.1:5000/" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# submodules/benchmarks/news_agent_benchmarks/helpers.py | ||
|
||
import requests | ||
import logging | ||
|
||
logging.basicConfig(level=logging.INFO) | ||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def ask_news_agent(article_text: str, url: str) -> dict: | ||
headers = {'Content-Type': 'application/json'} | ||
payload = { | ||
"prompt": { | ||
"role": "user", | ||
"content": f"Classify if this article is relevant to cryptocurrency price movements: {article_text}" | ||
} | ||
} | ||
response = requests.post(url, headers=headers, json=payload) | ||
if response.status_code == 200: | ||
return response.json() | ||
else: | ||
raise Exception(f"Request failed with status code {response.status_code}: {response.text}") | ||
|
||
|
||
def extract_classification(response: dict) -> str: | ||
if not isinstance(response, dict): | ||
logger.warning(f"Unexpected response type: {type(response)}") | ||
return "UNKNOWN" | ||
|
||
content = response.get('content') | ||
|
||
if content is None: | ||
logger.warning("Response content is None") | ||
return "UNKNOWN" | ||
|
||
if not isinstance(content, str): | ||
logger.warning(f"Unexpected content type: {type(content)}") | ||
return "UNKNOWN" | ||
|
||
content = content.upper() | ||
|
||
if "NOT RELEVANT" in content: | ||
return "NOT RELEVANT" | ||
elif "RELEVANT" in content: | ||
return "RELEVANT" | ||
else: | ||
logger.warning(f"Could not determine relevance from content: {content}") | ||
return "NOT RELEVANT" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
130 changes: 130 additions & 0 deletions
130
submodules/moragents_dockers/agents/src/news_agent/src/agent.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import json | ||
import logging | ||
import re | ||
import urllib.parse | ||
from news_agent.src.config import Config | ||
from news_agent.src.tools import clean_html, is_within_time_window, fetch_rss_feed | ||
import pyshorteners | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
class NewsAgent: | ||
def __init__(self, agent_info, llm, llm_ollama, embeddings, flask_app): | ||
self.agent_info = agent_info | ||
self.llm = llm | ||
self.flask_app = flask_app | ||
self.tools_provided = self.get_tools() | ||
self.url_shortener = pyshorteners.Shortener() | ||
logger.info("NewsAgent initialized") | ||
|
||
def get_tools(self): | ||
return [ | ||
{ | ||
"type": "function", | ||
"function": { | ||
"name": "fetch_crypto_news", | ||
"description": "Fetch and analyze cryptocurrency news for potential price impacts", | ||
"parameters": { | ||
"type": "object", | ||
"properties": { | ||
"coins": { | ||
"type": "array", | ||
"items": { | ||
"type": "string" | ||
}, | ||
"description": "List of cryptocurrency symbols to fetch news for" | ||
} | ||
}, | ||
"required": ["coins"] | ||
} | ||
} | ||
} | ||
] | ||
|
||
def check_relevance_and_summarize(self, title, content, coin): | ||
logger.info(f"Checking relevance for {coin}: {title}") | ||
prompt = Config.RELEVANCE_PROMPT.format(coin=coin, title=title, content=content) | ||
result = self.llm.create_chat_completion( | ||
messages=[{"role": "user", "content": prompt}], | ||
max_tokens=Config.LLM_MAX_TOKENS, | ||
temperature=Config.LLM_TEMPERATURE | ||
) | ||
return result['choices'][0]['message']['content'].strip() | ||
|
||
def process_rss_feed(self, feed_url, coin): | ||
logger.info(f"Processing RSS feed for {coin}: {feed_url}") | ||
feed = fetch_rss_feed(feed_url) | ||
results = [] | ||
for entry in feed.entries: | ||
published_time = entry.get('published') or entry.get('updated') | ||
if is_within_time_window(published_time): | ||
title = clean_html(entry.title) | ||
content = clean_html(entry.summary) | ||
logger.info(f"Checking relevance for article: {title}") | ||
result = self.check_relevance_and_summarize(title, content, coin) | ||
if not result.upper().startswith("NOT RELEVANT"): | ||
results.append({ | ||
"Title": title, | ||
"Summary": result, | ||
"Link": entry.link | ||
}) | ||
if len(results) >= Config.ARTICLES_PER_TOKEN: | ||
break | ||
else: | ||
logger.info(f"Skipping article: {entry.title} (published: {published_time})") | ||
logger.info(f"Found {len(results)} relevant articles for {coin}") | ||
return results | ||
|
||
def fetch_crypto_news(self, coins): | ||
logger.info(f"Fetching news for coins: {coins}") | ||
all_news = [] | ||
for coin in coins: | ||
logger.info(f"Processing news for {coin}") | ||
coin_name = Config.CRYPTO_DICT.get(coin.upper(), coin) | ||
google_news_url = Config.GOOGLE_NEWS_BASE_URL.format(coin_name) | ||
results = self.process_rss_feed(google_news_url, coin_name) | ||
all_news.extend([{"Coin": coin, **result} for result in results[:Config.ARTICLES_PER_TOKEN]]) | ||
|
||
logger.info(f"Total news items fetched: {len(all_news)}") | ||
return all_news | ||
|
||
def chat(self, request): | ||
try: | ||
data = request.get_json() | ||
if 'prompt' in data: | ||
prompt = data['prompt'] | ||
if isinstance(prompt, dict) and 'content' in prompt: | ||
prompt = prompt['content'] | ||
|
||
# Updated coin detection logic | ||
coins = re.findall(r'\b(' + '|'.join(re.escape(key) for key in Config.CRYPTO_DICT.keys()) + r')\b', | ||
prompt.upper()) | ||
|
||
if not coins: | ||
return {"role": "assistant", | ||
"content": "I couldn't identify any cryptocurrency symbols in your message. Please specify the cryptocurrencies you want news for.", | ||
"next_turn_agent": None} | ||
|
||
news = self.fetch_crypto_news(coins) | ||
|
||
if not news: | ||
return {"role": "assistant", | ||
"content": "No relevant news found for the specified cryptocurrencies in the last 24 hours.", | ||
"next_turn_agent": None} | ||
|
||
response = "Here are the latest news items relevant to changes in price movement of the mentioned tokens in the last 24 hours:\n\n" | ||
for index, item in enumerate(news, start=1): | ||
coin_name = Config.CRYPTO_DICT.get(item['Coin'], item['Coin']) | ||
short_url = self.url_shortener.tinyurl.short(item['Link']) | ||
response += f"{index}. ***{coin_name} News***:\n" | ||
response += f"{item['Title']}\n" | ||
response += f"{item['Summary']}\n" | ||
response += f"Read more: {short_url}\n\n" | ||
|
||
return {"role": "assistant", "content": response, "next_turn_agent": None} | ||
else: | ||
return {"role": "assistant", "content": "Missing required parameters", "next_turn_agent": None} | ||
|
||
except Exception as e: | ||
logger.error(f"Error in chat method: {str(e)}", exc_info=True) | ||
return {"role": "assistant", "content": f"An error occurred: {str(e)}", "next_turn_agent": None} |
Oops, something went wrong.