AJAY KASU commited on
Commit
5dafb0f
·
1 Parent(s): c789300

Refactor: Migrate LLM integration to Bytez AI API

Browse files
Files changed (3) hide show
  1. ai/ai_reporter.py +90 -66
  2. config.py +1 -0
  3. requirements.txt +1 -0
ai/ai_reporter.py CHANGED
@@ -1,61 +1,122 @@
1
  import logging
2
- from huggingface_hub import InferenceClient
 
 
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 Hugging Face Inference API.
12
- Models used: meta-llama/Meta-Llama-3-8B-Instruct (or similar available via API).
13
  """
14
 
15
  def __init__(self):
16
- token = settings.HF_TOKEN.get_secret_value() if settings.HF_TOKEN else None
 
17
 
18
- if token:
19
- self.client = InferenceClient(token=token)
20
- else:
21
- self.client = None
22
- logger.warning("HF_TOKEN not found. AI features will be disabled.")
23
 
24
- # Default to a robust instruction model
25
- self.model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def generate_report(self,
28
  attribution_report: AttributionReport,
29
  excluded_sector: str) -> str:
30
  """
31
- Constructs the prompt and calls the HF API to generate the commentary.
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, # Convert to %
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), # Truth Table Injection
54
- current_date=current_date # Pass date to template
55
  )
56
 
57
- if not self.client:
58
- return f"AI Commentary Unavailable. (Missing HF_TOKEN). Current Date: {current_date}"
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
- response = self.client.chat_completion(
67
- model=self.model_id,
68
- messages=messages,
69
- max_tokens=500,
70
- temperature=0.7
71
- )
72
 
73
- commentary = response.choices[0].message.content
74
- logger.info("AI Commentary generated successfully.")
 
75
  return commentary
76
 
77
  except Exception as e:
78
- logger.error(f"Failed to generate AI report: {e}")
79
- return "Error generating commentary. Please check API connection."
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