Refactor news & social modules

- update NewsAPIsTool
- update SocialAPIsTool
- add tests for NewsAPIsTool
- added some missing docs
This commit is contained in:
2025-09-30 22:13:12 +02:00
parent 3bc24afcea
commit 01e7bf58f1
8 changed files with 131 additions and 9 deletions

View File

View File

@@ -2,7 +2,7 @@ from agno.tools import Toolkit
from app.utils.wrapper_handler import WrapperHandler
from .base import NewsWrapper, Article
from .news_api import NewsApiWrapper
from .gnews_api import GoogleNewsWrapper
from .googlenews import GoogleNewsWrapper
from .cryptopanic_api import CryptoPanicWrapper
from .duckduckgo import DuckDuckGoWrapper
@@ -24,6 +24,15 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
"""
def __init__(self):
"""
Initialize the NewsAPIsTool with multiple news API wrappers.
The tool uses WrapperHandler to manage and invoke the different news API wrappers.
The following wrappers are included in this order:
- GoogleNewsWrapper.
- DuckDuckGoWrapper.
- NewsApiWrapper.
- CryptoPanicWrapper.
"""
wrappers = [GoogleNewsWrapper, DuckDuckGoWrapper, NewsApiWrapper, CryptoPanicWrapper]
self.wrapper_handler: WrapperHandler[NewsWrapper] = WrapperHandler.build_wrappers(wrappers)

View File

@@ -1 +0,0 @@
from .base import SocialWrapper

View File

@@ -0,0 +1,52 @@
from .base import SocialPost, SocialWrapper
from .reddit import RedditWrapper
from app.utils.wrapper_handler import WrapperHandler
from agno.tools import Toolkit
__all__ = ["SocialAPIsTool", "SOCIAL_INSTRUCTIONS", "RedditWrapper"]
class SocialAPIsTool(SocialWrapper, Toolkit):
"""
Aggregates multiple social media API wrappers and manages them using WrapperHandler.
This class supports retrieving top crypto-related posts by querying multiple sources:
- RedditWrapper
By default, it returns results from the first successful wrapper.
Optionally, it can be configured to collect posts from all wrappers.
If no wrapper succeeds, an exception is raised.
"""
def __init__(self):
"""
Initialize the SocialAPIsTool with multiple social media API wrappers.
The tool uses WrapperHandler to manage and invoke the different social media API wrappers.
The following wrappers are included in this order:
- RedditWrapper.
"""
wrappers = [RedditWrapper]
self.wrapper_handler: WrapperHandler[SocialWrapper] = WrapperHandler(wrappers)
Toolkit.__init__(
self,
name="Socials Toolkit",
tools=[
self.get_top_crypto_posts,
],
)
# TODO Pensare se ha senso restituire i post da TUTTI i wrapper o solo dal primo che funziona
# la modifica è banale, basta usare try_call_all invece di try_call
def get_top_crypto_posts(self, limit:int = 5) -> list[SocialPost]:
return self.wrapper_handler.try_call(lambda w: w.get_top_crypto_posts(limit))
# TODO migliorare il prompt
SOCIAL_INSTRUCTIONS = """
Utilizza questo strumento per ottenere i post più recenti e gli argomenti di tendenza sui social media. Puoi richiedere i post più recenti o gli argomenti di tendenza.
Esempio di utilizzo:
- get_latest_news("crypto", limit=5) # ottieni le ultime 5 notizie su "crypto", la query può essere qualsiasi argomento di interesse
- get_top_headlines(limit=3) # ottieni i 3 titoli principali delle notizie globali
"""

View File

@@ -7,16 +7,23 @@ class SocialPost(BaseModel):
description: str = ""
comments: list["SocialComment"] = []
def __str__(self):
return f"Title: {self.title}\nDescription: {self.description}\nComments: {len(self.comments)}\n[{" | ".join(str(c) for c in self.comments)}]"
class SocialComment(BaseModel):
time: str = ""
description: str = ""
def __str__(self):
return f"Time: {self.time}\nDescription: {self.description}"
# TODO IMPLEMENTARLO SE SI USANO PIU' WRAPPER (E QUINDI PIU' SOCIAL)
class SocialWrapper:
pass
"""
Base class for social media API wrappers.
All social media API wrappers should inherit from this class and implement the methods.
"""
def get_top_crypto_posts(self, limit: int = 5) -> list[SocialPost]:
"""
Get top cryptocurrency-related posts, optionally limited by total.
Args:
limit (int): The maximum number of posts to return.
Returns:
list[SocialPost]: A list of SocialPost objects.
"""
raise NotImplementedError("This method should be overridden by subclasses")

View File

@@ -23,6 +23,7 @@ def pytest_configure(config:pytest.Config):
("social", "marks tests that use social media"),
("limited", "marks tests that have limited execution due to API constraints"),
("wrapper", "marks tests for wrapper handler"),
("tools", "marks tests for tools"),
]
for marker in markers:
line = f"{marker[0]}: {marker[1]}"

View File

@@ -0,0 +1,54 @@
import pytest
from app.news import NewsAPIsTool
@pytest.mark.limited
@pytest.mark.tools
@pytest.mark.news
@pytest.mark.api
class TestNewsAPITool:
def test_news_api_tool(self):
tool = NewsAPIsTool()
assert tool is not None
def test_news_api_tool_get_top(self):
tool = NewsAPIsTool()
result = tool.wrapper_handler.try_call(lambda w: w.get_top_headlines(total=2))
assert isinstance(result, list)
assert len(result) > 0
for article in result:
assert article.title is not None
assert article.source is not None
def test_news_api_tool_get_latest(self):
tool = NewsAPIsTool()
result = tool.wrapper_handler.try_call(lambda w: w.get_latest_news(query="crypto", total=2))
assert isinstance(result, list)
assert len(result) > 0
for article in result:
assert article.title is not None
assert article.source is not None
def test_news_api_tool_get_top__all_results(self):
tool = NewsAPIsTool()
result = tool.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(total=2))
assert isinstance(result, dict)
assert len(result.keys()) > 0
print("Results from providers:", result.keys())
for provider, articles in result.items():
for article in articles:
print(provider, article.title)
assert article.title is not None
assert article.source is not None
def test_news_api_tool_get_latest__all_results(self):
tool = NewsAPIsTool()
result = tool.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query="crypto", total=2))
assert isinstance(result, dict)
assert len(result.keys()) > 0
print("Results from providers:", result.keys())
for provider, articles in result.items():
for article in articles:
print(provider, article.title)
assert article.title is not None
assert article.source is not None