Spaces:
Sleeping
Sleeping
AJAY KASU commited on
Commit ·
5dafb0f
1
Parent(s): c789300
Refactor: Migrate LLM integration to Bytez AI API
Browse files- ai/ai_reporter.py +90 -66
- config.py +1 -0
- requirements.txt +1 -0
ai/ai_reporter.py
CHANGED
|
@@ -1,61 +1,122 @@
|
|
| 1 |
import logging
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
from core.schema import AttributionReport
|
| 4 |
-
from ai.prompts import SYSTEM_PROMPT, ATTRIBUTION_PROMPT_TEMPLATE
|
| 5 |
from config import settings
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
| 9 |
class AIReporter:
|
| 10 |
"""
|
| 11 |
-
Generates natural language commentary using
|
| 12 |
-
|
| 13 |
"""
|
| 14 |
|
| 15 |
def __init__(self):
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
if
|
| 19 |
-
|
| 20 |
-
else:
|
| 21 |
-
self.client = None
|
| 22 |
-
logger.warning("HF_TOKEN not found. AI features will be disabled.")
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
def generate_report(self,
|
| 28 |
attribution_report: AttributionReport,
|
| 29 |
excluded_sector: str) -> str:
|
| 30 |
"""
|
| 31 |
-
Constructs the prompt and calls the
|
| 32 |
"""
|
| 33 |
-
logger.info("Generating AI Commentary...")
|
| 34 |
|
| 35 |
from datetime import datetime
|
| 36 |
-
# Get current date in a specific format (e.g., "February 03, 2026")
|
| 37 |
current_date = datetime.now().strftime("%B %d, %Y")
|
| 38 |
|
| 39 |
-
import json
|
| 40 |
-
|
| 41 |
# Format the user prompt
|
| 42 |
-
# We assume ATTRIBUTION_PROMPT_TEMPLATE handles the rest, but we force the date in context
|
| 43 |
user_prompt = f"""
|
| 44 |
Current Date: {current_date}
|
| 45 |
INSTRUCTION: Start your commentary exactly with the header: "Market Commentary - {current_date}"
|
| 46 |
""" + ATTRIBUTION_PROMPT_TEMPLATE.format(
|
| 47 |
excluded_sector=excluded_sector,
|
| 48 |
-
total_active_return=attribution_report.total_active_return * 100,
|
| 49 |
allocation_effect=attribution_report.allocation_effect * 100,
|
| 50 |
selection_effect=attribution_report.selection_effect * 100,
|
| 51 |
top_contributors=json.dumps(attribution_report.top_contributors, indent=2),
|
| 52 |
top_detractors=json.dumps(attribution_report.top_detractors, indent=2),
|
| 53 |
-
sector_positioning=json.dumps(attribution_report.sector_exposure, indent=2),
|
| 54 |
-
current_date=current_date
|
| 55 |
)
|
| 56 |
|
| 57 |
-
if not self.
|
| 58 |
-
return f"AI Commentary Unavailable. (Missing
|
| 59 |
|
| 60 |
messages = [
|
| 61 |
{"role": "system", "content": SYSTEM_PROMPT},
|
|
@@ -63,50 +124,13 @@ INSTRUCTION: Start your commentary exactly with the header: "Market Commentary -
|
|
| 63 |
]
|
| 64 |
|
| 65 |
try:
|
| 66 |
-
|
| 67 |
-
model=self.model_id,
|
| 68 |
-
messages=messages,
|
| 69 |
-
max_tokens=500,
|
| 70 |
-
temperature=0.7
|
| 71 |
-
)
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
|
|
|
| 75 |
return commentary
|
| 76 |
|
| 77 |
except Exception as e:
|
| 78 |
-
logger.error(f"Failed to generate
|
| 79 |
-
return "Error generating commentary
|
| 80 |
-
def parse_intent(self, user_prompt: str) -> list:
|
| 81 |
-
"""
|
| 82 |
-
Uses LLM to map user prompt to a list of exact GICS sectors to exclude.
|
| 83 |
-
"""
|
| 84 |
-
if not self.client:
|
| 85 |
-
logger.warning("LLM Client unavailable for Intent Parsing. Falling back to empty list.")
|
| 86 |
-
return []
|
| 87 |
-
|
| 88 |
-
from ai.prompts import INTENT_PARSER_SYSTEM_PROMPT
|
| 89 |
-
|
| 90 |
-
try:
|
| 91 |
-
response = self.client.chat_completion(
|
| 92 |
-
model=self.model_id,
|
| 93 |
-
messages=[
|
| 94 |
-
{"role": "system", "content": INTENT_PARSER_SYSTEM_PROMPT},
|
| 95 |
-
{"role": "user", "content": f"Parse this prompt for sector exclusions: '{user_prompt}'"}
|
| 96 |
-
],
|
| 97 |
-
max_tokens=100,
|
| 98 |
-
temperature=0.0 # Strict output
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
content = response.choices[0].message.content.strip()
|
| 102 |
-
# Find the JSON list in the response
|
| 103 |
-
import re
|
| 104 |
-
match = re.search(r'\[.*\]', content, re.DOTALL)
|
| 105 |
-
if match:
|
| 106 |
-
import json
|
| 107 |
-
return json.loads(match.group(0))
|
| 108 |
-
return []
|
| 109 |
-
|
| 110 |
-
except Exception as e:
|
| 111 |
-
logger.error(f"Intent Parsing failed: {e}")
|
| 112 |
-
return []
|
|
|
|
| 1 |
import logging
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import requests
|
| 5 |
from core.schema import AttributionReport
|
| 6 |
+
from ai.prompts import SYSTEM_PROMPT, ATTRIBUTION_PROMPT_TEMPLATE, INTENT_PARSER_SYSTEM_PROMPT
|
| 7 |
from config import settings
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
| 11 |
class AIReporter:
|
| 12 |
"""
|
| 13 |
+
Generates natural language commentary using Bytez AI API.
|
| 14 |
+
Replaces Hugging Face InferenceClient for more reliable performance.
|
| 15 |
"""
|
| 16 |
|
| 17 |
def __init__(self):
|
| 18 |
+
# Read the API key from environment (prioritize settings/os.environ)
|
| 19 |
+
self.api_key = settings.BYTEZ_API_KEY.get_secret_value() if settings.BYTEZ_API_KEY else os.environ.get("BYTEZ_API_KEY")
|
| 20 |
|
| 21 |
+
if not self.api_key:
|
| 22 |
+
logger.warning("BYTEZ_API_KEY not found in environment. AI features will be disabled.")
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
# Using Llama 3 8B Instruct as the baseline model on Bytez
|
| 25 |
+
self.base_url = "https://api.bytez.com/models/v2"
|
| 26 |
+
self.model_path = "meta-llama/Meta-Llama-3-8B-Instruct"
|
| 27 |
+
self.endpoint = f"{self.base_url}/{self.model_path}"
|
| 28 |
+
|
| 29 |
+
def _call_bytez(self, messages: list, max_tokens: int = 500, temperature: float = 0.7) -> str:
|
| 30 |
+
"""
|
| 31 |
+
Helper to make the POST request to Bytez.
|
| 32 |
+
"""
|
| 33 |
+
if not self.api_key:
|
| 34 |
+
return ""
|
| 35 |
+
|
| 36 |
+
headers = {
|
| 37 |
+
"Content-Type": "application/json",
|
| 38 |
+
"Authorization": self.api_key
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
payload = {
|
| 42 |
+
"messages": messages,
|
| 43 |
+
"max_tokens": max_tokens,
|
| 44 |
+
"temperature": temperature
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
response = requests.post(self.endpoint, headers=headers, json=payload, timeout=30)
|
| 49 |
+
response.raise_for_status()
|
| 50 |
+
|
| 51 |
+
result = response.json()
|
| 52 |
+
# Handle different common response formats
|
| 53 |
+
if isinstance(result, dict) and "choices" in result:
|
| 54 |
+
return result["choices"][0]["message"]["content"]
|
| 55 |
+
elif isinstance(result, str):
|
| 56 |
+
return result
|
| 57 |
+
else:
|
| 58 |
+
return str(result)
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Bytez API Call Failed: {e}")
|
| 62 |
+
return ""
|
| 63 |
+
|
| 64 |
+
def parse_intent(self, user_prompt: str) -> list:
|
| 65 |
+
"""
|
| 66 |
+
Uses Bytez AI to map user prompt to a list of exact GICS sectors to exclude.
|
| 67 |
+
"""
|
| 68 |
+
logger.info(f"Parsing intent with Bytez for prompt: {user_prompt[:50]}...")
|
| 69 |
+
|
| 70 |
+
messages = [
|
| 71 |
+
{"role": "system", "content": INTENT_PARSER_SYSTEM_PROMPT},
|
| 72 |
+
{"role": "user", "content": f"Parse this prompt for sector exclusions: '{user_prompt}'"}
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
content = self._call_bytez(messages, max_tokens=100, temperature=0.0)
|
| 77 |
+
if not content:
|
| 78 |
+
logger.warning("Empty response from Bytez for Intent Parsing. Returning empty list.")
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
# Clean content for JSON extraction
|
| 82 |
+
import re
|
| 83 |
+
match = re.search(r'\[.*\]', content.strip(), re.DOTALL)
|
| 84 |
+
if match:
|
| 85 |
+
return json.loads(match.group(0))
|
| 86 |
+
return []
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Intent Parsing Error (Bytez): {e}")
|
| 90 |
+
return []
|
| 91 |
|
| 92 |
def generate_report(self,
|
| 93 |
attribution_report: AttributionReport,
|
| 94 |
excluded_sector: str) -> str:
|
| 95 |
"""
|
| 96 |
+
Constructs the prompt and calls the Bytez API to generate the commentary.
|
| 97 |
"""
|
| 98 |
+
logger.info("Generating AI Commentary with Bytez...")
|
| 99 |
|
| 100 |
from datetime import datetime
|
|
|
|
| 101 |
current_date = datetime.now().strftime("%B %d, %Y")
|
| 102 |
|
|
|
|
|
|
|
| 103 |
# Format the user prompt
|
|
|
|
| 104 |
user_prompt = f"""
|
| 105 |
Current Date: {current_date}
|
| 106 |
INSTRUCTION: Start your commentary exactly with the header: "Market Commentary - {current_date}"
|
| 107 |
""" + ATTRIBUTION_PROMPT_TEMPLATE.format(
|
| 108 |
excluded_sector=excluded_sector,
|
| 109 |
+
total_active_return=attribution_report.total_active_return * 100,
|
| 110 |
allocation_effect=attribution_report.allocation_effect * 100,
|
| 111 |
selection_effect=attribution_report.selection_effect * 100,
|
| 112 |
top_contributors=json.dumps(attribution_report.top_contributors, indent=2),
|
| 113 |
top_detractors=json.dumps(attribution_report.top_detractors, indent=2),
|
| 114 |
+
sector_positioning=json.dumps(attribution_report.sector_exposure, indent=2),
|
| 115 |
+
current_date=current_date
|
| 116 |
)
|
| 117 |
|
| 118 |
+
if not self.api_key:
|
| 119 |
+
return f"AI Commentary Unavailable. (Missing BYTEZ_API_KEY). Current Date: {current_date}"
|
| 120 |
|
| 121 |
messages = [
|
| 122 |
{"role": "system", "content": SYSTEM_PROMPT},
|
|
|
|
| 124 |
]
|
| 125 |
|
| 126 |
try:
|
| 127 |
+
commentary = self._call_bytez(messages, max_tokens=600, temperature=0.7)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
if not commentary:
|
| 130 |
+
return "AI Commentary generation timed out or failed. Please try again."
|
| 131 |
+
|
| 132 |
return commentary
|
| 133 |
|
| 134 |
except Exception as e:
|
| 135 |
+
logger.error(f"Failed to generate Bytez report: {e}")
|
| 136 |
+
return "Error generating commentary via Bytez AI. Check API key and connectivity."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
CHANGED
|
@@ -10,6 +10,7 @@ class Settings(BaseModel):
|
|
| 10 |
|
| 11 |
# API Keys
|
| 12 |
HF_TOKEN: Optional[SecretStr] = Field(default_factory=lambda: SecretStr(os.getenv("HF_TOKEN", "")) if os.getenv("HF_TOKEN") else None, description="Hugging Face API Token")
|
|
|
|
| 13 |
|
| 14 |
# Data Configuration
|
| 15 |
DATA_DIR: str = Field(default="./data", description="Directory to store static data files")
|
|
|
|
| 10 |
|
| 11 |
# API Keys
|
| 12 |
HF_TOKEN: Optional[SecretStr] = Field(default_factory=lambda: SecretStr(os.getenv("HF_TOKEN", "")) if os.getenv("HF_TOKEN") else None, description="Hugging Face API Token")
|
| 13 |
+
BYTEZ_API_KEY: Optional[SecretStr] = Field(default_factory=lambda: SecretStr(os.getenv("BYTEZ_API_KEY", "")) if os.getenv("BYTEZ_API_KEY") else None, description="Bytez API Key")
|
| 14 |
|
| 15 |
# Data Configuration
|
| 16 |
DATA_DIR: str = Field(default="./data", description="Directory to store static data files")
|
requirements.txt
CHANGED
|
@@ -13,3 +13,4 @@ matplotlib>=3.8.2
|
|
| 13 |
scipy>=1.11.4
|
| 14 |
huggingface_hub>=0.20.0
|
| 15 |
streamlit>=1.30.0
|
|
|
|
|
|
| 13 |
scipy>=1.11.4
|
| 14 |
huggingface_hub>=0.20.0
|
| 15 |
streamlit>=1.30.0
|
| 16 |
+
requests>=2.31.0
|