3 market api #8

Merged
Simo93-rgb merged 25 commits from 3-market-api into main 2025-10-01 15:51:25 +02:00
11 changed files with 72 additions and 46 deletions
Showing only changes of commit c82f10b32c - Show all commits

View File

@@ -16,7 +16,7 @@ dependencies = [
"dotenv", "dotenv",
# 🟡 per fare scraping di pagine web # 🟡 per fare scraping di pagine web
#"bs4", #"bs4",
# ✅ per fare una UI web semplice con input e output # ✅ per fare una UI web semplice con user_input e output
"gradio", "gradio",
# ✅ per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno # ✅ per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno

View File

@@ -1,5 +1,5 @@
from agno.tools import Toolkit from agno.tools import Toolkit
from app.markets import MarketAPIs from src.app.markets import MarketAPIs
# TODO (?) in futuro fare in modo che la LLM faccia da sé per il mercato # TODO (?) in futuro fare in modo che la LLM faccia da sé per il mercato
# Non so se può essere utile, per ora lo lascio qui # Non so se può essere utile, per ora lo lascio qui

View File

@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from app.markets.base import ProductInfo from src.app.markets.base import ProductInfo
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class PredictorStyle(Enum): class PredictorStyle(Enum):
@@ -23,7 +23,7 @@ class PredictorOutput(BaseModel):
PREDICTOR_INSTRUCTIONS = """ PREDICTOR_INSTRUCTIONS = """
You are an **Allocation Algorithm (Crypto-Algo)** specialized in analyzing market data and sentiment to generate an investment strategy and a target portfolio. You are an **Allocation Algorithm (Crypto-Algo)** specialized in analyzing market data and sentiment to generate an investment strategy and a target portfolio.
Your sole objective is to process the input data and generate the strictly structured output as required by the response format. **You MUST NOT provide introductions, preambles, explanations, conclusions, or any additional comments that are not strictly required.** Your sole objective is to process the user_input data and generate the strictly structured output as required by the response format. **You MUST NOT provide introductions, preambles, explanations, conclusions, or any additional comments that are not strictly required.**
## Processing Instructions (Absolute Rule) ## Processing Instructions (Absolute Rule)

View File

@@ -1,6 +1,6 @@
from app.markets.base import BaseWrapper from src.app.markets.base import BaseWrapper
from app.markets.coinbase import CoinBaseWrapper from src.app.markets.coinbase import CoinBaseWrapper
from app.markets.cryptocompare import CryptoCompareWrapper from src.app.markets.cryptocompare import CryptoCompareWrapper
from agno.utils.log import log_warning from agno.utils.log import log_warning
@@ -30,8 +30,8 @@ class MarketAPIs(BaseWrapper):
for wrapper in wrapper_builders: for wrapper in wrapper_builders:
try: try:
result.append(wrapper(currency=currency)) result.append(wrapper(currency=currency))
except Exception as _: except Exception as e:
log_warning(f"{wrapper} cannot be initialized, maybe missing API key?") log_warning(f"{wrapper} cannot be initialized: {e}")
assert result, "No market API keys set in environment variables." assert result, "No market API keys set in environment variables."
return result return result
@@ -39,7 +39,9 @@ class MarketAPIs(BaseWrapper):
def __init__(self, currency: str = "USD"): def __init__(self, currency: str = "USD"):
""" """
Inizializza la classe con la valuta di riferimento e la priorità dei provider. Inizializza la classe con la valuta di riferimento e la priorità dei provider.
:param currency: Valuta di riferimento (default "USD")
Args:
currency: Valuta di riferimento (default "USD")
""" """
self.currency = currency self.currency = currency
self.wrappers = MarketAPIs.get_list_available_market_apis(currency=currency) self.wrappers = MarketAPIs.get_list_available_market_apis(currency=currency)

View File

@@ -1,6 +1,6 @@
import os import os
from coinbase.rest import RESTClient from coinbase.rest import RESTClient
from app.markets.base import ProductInfo, BaseWrapper, Price from src.app.markets.base import ProductInfo, BaseWrapper, Price
class CoinBaseWrapper(BaseWrapper): class CoinBaseWrapper(BaseWrapper):
""" """

View File

@@ -1,6 +1,6 @@
import os import os
import requests import requests
from app.markets.base import ProductInfo, BaseWrapper, Price from src.app.markets.base import ProductInfo, BaseWrapper, Price
BASE_URL = "https://min-api.cryptocompare.com" BASE_URL = "https://min-api.cryptocompare.com"
@@ -8,7 +8,7 @@ class CryptoCompareWrapper(BaseWrapper):
""" """
Wrapper per le API pubbliche di CryptoCompare. Wrapper per le API pubbliche di CryptoCompare.
La documentazione delle API è disponibile qui: https://developers.coindesk.com/documentation/legacy/Price/SingleSymbolPriceEndpoint La documentazione delle API è disponibile qui: https://developers.coindesk.com/documentation/legacy/Price/SingleSymbolPriceEndpoint
!!ATTENZIONE!! sembra essere una API legacy e potrebbe essere deprecata in futuro. !ATTENZIONE! Sembra essere una API legacy e potrebbe essere deprecata in futuro.
""" """
def __init__(self, api_key:str = None, currency:str='USD'): def __init__(self, api_key:str = None, currency:str='USD'):
if api_key is None: if api_key is None:

View File

@@ -41,6 +41,7 @@ class AppModels(Enum):
availables.append(AppModels.OLLAMA_QWEN) availables.append(AppModels.OLLAMA_QWEN)
return availables return availables
@staticmethod
def availables_online() -> list['AppModels']: def availables_online() -> list['AppModels']:
""" """
Controlla quali provider di modelli LLM online hanno le loro API keys disponibili Controlla quali provider di modelli LLM online hanno le loro API keys disponibili
@@ -49,9 +50,7 @@ class AppModels(Enum):
if not os.getenv("GOOGLE_API_KEY"): if not os.getenv("GOOGLE_API_KEY"):
log_warning("No GOOGLE_API_KEY set in environment variables.") log_warning("No GOOGLE_API_KEY set in environment variables.")
return [] return []
availables = [] availables = [AppModels.GEMINI, AppModels.GEMINI_PRO]
availables.append(AppModels.GEMINI)
availables.append(AppModels.GEMINI_PRO)
return availables return availables
@staticmethod @staticmethod
@@ -75,9 +74,13 @@ class AppModels(Enum):
def extract_json_str_from_response(response: str) -> str: def extract_json_str_from_response(response: str) -> str:
""" """
Estrae il JSON dalla risposta del modello. Estrae il JSON dalla risposta del modello.
Args:
response: risposta del modello (stringa). response: risposta del modello (stringa).
Ritorna la parte JSON della risposta come stringa.
Returns:
La parte JSON della risposta come stringa.
Se non viene trovato nessun JSON, ritorna una stringa vuota. Se non viene trovato nessun JSON, ritorna una stringa vuota.
ATTENZIONE: questa funzione è molto semplice e potrebbe non funzionare ATTENZIONE: questa funzione è molto semplice e potrebbe non funzionare
in tutti i casi. Si assume che il JSON sia ben formato e che inizi con in tutti i casi. Si assume che il JSON sia ben formato e che inizi con
'{' e finisca con '}'. Quindi anche solo un json array farà fallire questa funzione. '{' e finisca con '}'. Quindi anche solo un json array farà fallire questa funzione.
@@ -98,9 +101,15 @@ class AppModels(Enum):
def get_model(self, instructions:str) -> Model: def get_model(self, instructions:str) -> Model:
""" """
Restituisce un'istanza del modello specificato. Restituisce un'istanza del modello specificato.
Args:
instructions: istruzioni da passare al modello (system prompt). instructions: istruzioni da passare al modello (system prompt).
Ritorna un'istanza di BaseModel o una sua sottoclasse.
Raise ValueError se il modello non è supportato. Returns:
Un'istanza di BaseModel o una sua sottoclasse.
Raise:
ValueError se il modello non è supportato.
""" """
name = self.value name = self.value
if self in {AppModels.GEMINI, AppModels.GEMINI_PRO}: if self in {AppModels.GEMINI, AppModels.GEMINI_PRO}:
@@ -113,8 +122,13 @@ class AppModels(Enum):
def get_agent(self, instructions: str, name: str = "", output: BaseModel | None = None) -> Agent: def get_agent(self, instructions: str, name: str = "", output: BaseModel | None = None) -> Agent:
""" """
Costruisce un agente con il modello e le istruzioni specificate. Costruisce un agente con il modello e le istruzioni specificate.
instructions: istruzioni da passare al modello (system prompt). Args:
Ritorna un'istanza di Agent. instructions: istruzioni da passare al modello (system prompt)
name: nome dell'agente (opzionale)
output: schema di output opzionale (Pydantic BaseModel)
Returns:
Un'istanza di Agent.
""" """
return Agent( return Agent(
model=self.get_model(instructions), model=self.get_model(instructions),

View File

@@ -1,8 +1,8 @@
from app.agents.news_agent import NewsAgent from src.app.agents.news_agent import NewsAgent
from app.agents.social_agent import SocialAgent from src.app.agents.social_agent import SocialAgent
from app.agents.predictor import PredictorStyle, PredictorInput, PredictorOutput, PREDICTOR_INSTRUCTIONS from src.app.agents.predictor import PredictorStyle, PredictorInput, PredictorOutput, PREDICTOR_INSTRUCTIONS
from app.markets import MarketAPIs from src.app.markets import MarketAPIs
from app.models import AppModels from src.app.models import AppModels
from agno.utils.log import log_info from agno.utils.log import log_info
class ToolAgent: class ToolAgent:
@@ -14,6 +14,10 @@ class ToolAgent:
""" """
Inizializza l'agente con i modelli disponibili, gli stili e l'API di mercato. Inizializza l'agente con i modelli disponibili, gli stili e l'API di mercato.
""" """
self.social_agent = None
self.news_agent = None
self.predictor = None
self.chosen_model = None
self.available_models = AppModels.availables() self.available_models = AppModels.availables()
self.all_styles = list(PredictorStyle) self.all_styles = list(PredictorStyle)
self.style = self.all_styles[0] # Default to the first style self.style = self.all_styles[0] # Default to the first style
@@ -24,6 +28,8 @@ class ToolAgent:
def choose_provider(self, index: int): def choose_provider(self, index: int):
""" """
Sceglie il modello LLM da utilizzare in base all'indice fornito. Sceglie il modello LLM da utilizzare in base all'indice fornito.
Args:
index: indice del modello nella lista available_models. index: indice del modello nella lista available_models.
""" """
# TODO Utilizzare AGNO per gestire i modelli... è molto più semplice e permette di cambiare modello facilmente # TODO Utilizzare AGNO per gestire i modelli... è molto più semplice e permette di cambiare modello facilmente
@@ -37,6 +43,8 @@ class ToolAgent:
def choose_style(self, index: int): def choose_style(self, index: int):
""" """
Sceglie lo stile di previsione da utilizzare in base all'indice fornito. Sceglie lo stile di previsione da utilizzare in base all'indice fornito.
Args:
index: indice dello stile nella lista all_styles. index: indice dello stile nella lista all_styles.
""" """
self.style = self.all_styles[index] self.style = self.all_styles[index]
@@ -44,8 +52,9 @@ class ToolAgent:
def interact(self, query: str) -> str: def interact(self, query: str) -> str:
""" """
Funzione principale che coordina gli agenti per rispondere alla richiesta dell'utente. Funzione principale che coordina gli agenti per rispondere alla richiesta dell'utente.
Args:
query: richiesta dell'utente (es. "Qual è la previsione per Bitcoin?") query: richiesta dell'utente (es. "Qual è la previsione per Bitcoin?")
style_index: indice dello stile di previsione nella lista all_styles.
""" """
log_info(f"[model={self.chosen_model.name}] [style={self.style.name}] [query=\"{query.replace('"', "'")}\"]") log_info(f"[model={self.chosen_model.name}] [style={self.style.name}] [query=\"{query.replace('"', "'")}\"]")

View File

@@ -1,5 +1,5 @@
import statistics import statistics
from typing import Dict, List, Any from typing import Dict, Any
class MarketAggregator: class MarketAggregator:
""" """
@@ -65,6 +65,7 @@ class MarketAggregator:
return float(v[:-1]) * 1_000 return float(v[:-1]) * 1_000
try: try:
return float(v) return float(v)
except Exception: except Exception as e:
print(f"Errore nel parsing del volume: {e}")
return 0.0 return 0.0
return 0.0 return 0.0

View File

@@ -1,10 +1,10 @@
import os import os
import pytest import pytest
from app.agents.market import MarketToolkit from src.app.agents.market import MarketToolkit
from app.markets.base import BaseWrapper from src.app.markets.base import BaseWrapper
from app.markets.coinbase import CoinBaseWrapper from src.app.markets.coinbase import CoinBaseWrapper
from app.markets.cryptocompare import CryptoCompareWrapper from src.app.markets.cryptocompare import CryptoCompareWrapper
from app.markets import MarketAPIs from src.app.markets import MarketAPIs
class TestMarketSystem: class TestMarketSystem:
"""Test suite per il sistema di mercato (wrappers + toolkit)""" """Test suite per il sistema di mercato (wrappers + toolkit)"""

View File

@@ -1,11 +1,11 @@
import pytest import pytest
from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle from src.app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle
from app.markets.base import ProductInfo from src.app.markets.base import ProductInfo
from app.models import AppModels from src.app.models import AppModels
def unified_checks(model: AppModels, input): def unified_checks(model: AppModels, user_input):
llm = model.get_agent(PREDICTOR_INSTRUCTIONS, output=PredictorOutput) llm = model.get_agent(PREDICTOR_INSTRUCTIONS, output=PredictorOutput) # type: ignore[arg-type]
result = llm.run(input) result = llm.run(user_input)
content = result.content content = result.content
assert isinstance(content, PredictorOutput) assert isinstance(content, PredictorOutput)