prabalGaur commited on
Commit
ed9ecbf
·
verified ·
1 Parent(s): bfbc813

Deploy updated app and community contributions

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. _hf_capstone_repo/.gitattributes +2 -0
  3. _hf_capstone_repo/README.md +21 -0
  4. _hf_capstone_repo/agents/agent.py +33 -0
  5. _hf_capstone_repo/agents/autonomous_planning_agent.py +181 -0
  6. _hf_capstone_repo/agents/deals.py +134 -0
  7. _hf_capstone_repo/agents/deep_neural_network.py +101 -0
  8. _hf_capstone_repo/agents/ensemble_agent.py +51 -0
  9. _hf_capstone_repo/agents/evaluator.py +218 -0
  10. _hf_capstone_repo/agents/frontier_agent.py +86 -0
  11. _hf_capstone_repo/agents/items.py +52 -0
  12. _hf_capstone_repo/agents/messaging_agent.py +79 -0
  13. _hf_capstone_repo/agents/neural_network_agent.py +29 -0
  14. _hf_capstone_repo/agents/planning_agent.py +57 -0
  15. _hf_capstone_repo/agents/preprocessor.py +50 -0
  16. _hf_capstone_repo/agents/scanner_agent.py +176 -0
  17. _hf_capstone_repo/agents/specialist_agent.py +90 -0
  18. _hf_capstone_repo/app.py +25 -0
  19. _hf_capstone_repo/deal_agent_framework.py +119 -0
  20. _hf_capstone_repo/env_utils.py +14 -0
  21. _hf_capstone_repo/free_config.py +37 -0
  22. _hf_capstone_repo/log_utils.py +35 -0
  23. _hf_capstone_repo/memory.json +29 -0
  24. _hf_capstone_repo/price_is_right.py +208 -0
  25. _hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/data_level0.bin +3 -0
  26. _hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/header.bin +3 -0
  27. _hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/index_metadata.pickle +3 -0
  28. _hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/length.bin +3 -0
  29. _hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/link_lists.bin +3 -0
  30. _hf_capstone_repo/products_vectorstore/chroma.sqlite3 +3 -0
  31. _hf_capstone_repo/requirements.txt +17 -0
  32. _hf_deploy/.gitattributes +2 -0
  33. _hf_deploy/README.md +21 -0
  34. _hf_deploy/agents/agent.py +33 -0
  35. _hf_deploy/agents/autonomous_planning_agent.py +181 -0
  36. _hf_deploy/agents/deals.py +134 -0
  37. _hf_deploy/agents/deep_neural_network.py +101 -0
  38. _hf_deploy/agents/ensemble_agent.py +51 -0
  39. _hf_deploy/agents/evaluator.py +218 -0
  40. _hf_deploy/agents/frontier_agent.py +86 -0
  41. _hf_deploy/agents/items.py +52 -0
  42. _hf_deploy/agents/messaging_agent.py +79 -0
  43. _hf_deploy/agents/neural_network_agent.py +29 -0
  44. _hf_deploy/agents/planning_agent.py +57 -0
  45. _hf_deploy/agents/preprocessor.py +50 -0
  46. _hf_deploy/agents/scanner_agent.py +176 -0
  47. _hf_deploy/agents/specialist_agent.py +90 -0
  48. _hf_deploy/app.py +25 -0
  49. _hf_deploy/deal_agent_framework.py +119 -0
  50. _hf_deploy/env_utils.py +14 -0
.gitattributes CHANGED
@@ -34,3 +34,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  products_vectorstore/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  products_vectorstore/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
37
+ _hf_capstone_repo/products_vectorstore/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
38
+ _hf_deploy/products_vectorstore/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
39
+ community_contributions/images/gui.png filter=lfs diff=lfs merge=lfs -text
40
+ community_contributions/images/metrics.png filter=lfs diff=lfs merge=lfs -text
41
+ space_staging/products_vectorstore/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
_hf_capstone_repo/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ products_vectorstore/** filter=lfs diff=lfs merge=lfs -text
_hf_capstone_repo/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Capstone - The Price Is Right
3
+ emoji: 💰
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "5.47.2"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Capstone — The Price Is Right
14
+
15
+ Multi-agent deal hunting AI for the LLM Engineering capstone project.
16
+
17
+ - Scans DealNews RSS feeds for deals
18
+ - Estimates true product prices with RAG + Groq + neural network
19
+ - Surfaces bargains in a live Gradio dashboard
20
+
21
+ **Secrets required:** `GROQ_API_KEY`, `HF_TOKEN`, `USE_MODAL_SPECIALIST=false`, `GRADIO_SERVER_NAME=0.0.0.0`
_hf_capstone_repo/agents/agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ class Agent:
4
+ """
5
+ An abstract superclass for Agents
6
+ Used to log messages in a way that can identify each Agent
7
+ """
8
+
9
+ # Foreground colors
10
+ RED = '\033[31m'
11
+ GREEN = '\033[32m'
12
+ YELLOW = '\033[33m'
13
+ BLUE = '\033[34m'
14
+ MAGENTA = '\033[35m'
15
+ CYAN = '\033[36m'
16
+ WHITE = '\033[37m'
17
+
18
+ # Background color
19
+ BG_BLACK = '\033[40m'
20
+
21
+ # Reset code to return to default color
22
+ RESET = '\033[0m'
23
+
24
+ name: str = ""
25
+ color: str = '\033[37m'
26
+
27
+ def log(self, message):
28
+ """
29
+ Log this as an info message, identifying the agent
30
+ """
31
+ color_code = self.BG_BLACK + self.color
32
+ message = f"[{self.name}] {message}"
33
+ logging.info(color_code + message + self.RESET)
_hf_capstone_repo/agents/autonomous_planning_agent.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List, Dict
2
+ from agents.agent import Agent
3
+ from agents.deals import Deal, Opportunity
4
+ from agents.scanner_agent import ScannerAgent
5
+ from agents.ensemble_agent import EnsembleAgent
6
+ from agents.messaging_agent import MessagingAgent
7
+ from openai import OpenAI
8
+ import json
9
+
10
+
11
+ class AutonomousPlanningAgent(Agent):
12
+ name = "Autonomous Planning Agent"
13
+ color = Agent.GREEN
14
+ MODEL = "gpt-5.1"
15
+
16
+ def __init__(self, collection):
17
+ """
18
+ Create instances of the 3 Agents that this planner coordinates across
19
+ """
20
+ self.log("Autonomous Planning Agent is initializing")
21
+ self.scanner = ScannerAgent()
22
+ self.ensemble = EnsembleAgent(collection)
23
+ self.messenger = MessagingAgent()
24
+ self.openai = OpenAI()
25
+ self.memory = None
26
+ self.opportunity = None
27
+ self.log("Autonomous Planning Agent is ready")
28
+
29
+ def scan_the_internet_for_bargains(self) -> str:
30
+ """
31
+ Run the tool to scan
32
+ """
33
+ self.log("Autonomous Planning agent is calling scanner")
34
+ results = self.scanner.scan(memory=self.memory)
35
+ return results.model_dump_json() if results else "No deals found"
36
+
37
+ def estimate_true_value(self, description: str) -> str:
38
+ """
39
+ Run the tool to estimate true value
40
+ """
41
+ self.log("Autonomous Planning agent is estimating value via Ensemble Agent")
42
+ estimate = self.ensemble.price(description)
43
+ return f"The estimated true value of {description} is {estimate}"
44
+
45
+ def notify_user_of_deal(
46
+ self, description: str, deal_price: float, estimated_true_value: float, url: str
47
+ ) -> Dict:
48
+ """
49
+ Run the tool to notify the user
50
+ """
51
+ if self.opportunity:
52
+ self.log("Autonomous Planning agent is trying to notify the user a 2nd time; ignoring")
53
+ else:
54
+ self.log("Autonomous Planning agent is notifying user")
55
+ self.messenger.notify(description, deal_price, estimated_true_value, url)
56
+ deal = Deal(product_description=description, price=deal_price, url=url)
57
+ discount = estimated_true_value - deal_price
58
+ self.opportunity = Opportunity(
59
+ deal=deal, estimate=estimated_true_value, discount=discount
60
+ )
61
+ return "Notification sent ok"
62
+
63
+ scan_function = {
64
+ "name": "scan_the_internet_for_bargains",
65
+ "description": "Returns top bargains scraped from the internet along with the price each item is being offered for",
66
+ "parameters": {
67
+ "type": "object",
68
+ "properties": {},
69
+ "required": [],
70
+ "additionalProperties": False,
71
+ },
72
+ }
73
+
74
+ estimate_function = {
75
+ "name": "estimate_true_value",
76
+ "description": "Given the description of an item, estimate how much it is actually worth",
77
+ "parameters": {
78
+ "type": "object",
79
+ "properties": {
80
+ "description": {
81
+ "type": "string",
82
+ "description": "The description of the item to be estimated",
83
+ },
84
+ },
85
+ "required": ["description"],
86
+ "additionalProperties": False,
87
+ },
88
+ }
89
+
90
+ notify_function = {
91
+ "name": "notify_user_of_deal",
92
+ "description": "Send the user a push notification about the single most compelling deal; only call this one time",
93
+ "parameters": {
94
+ "type": "object",
95
+ "properties": {
96
+ "description": {
97
+ "type": "string",
98
+ "description": "The description of the item itself scraped from the internet",
99
+ },
100
+ "deal_price": {
101
+ "type": "number",
102
+ "description": "The price offered by this deal scraped from the internet",
103
+ },
104
+ "estimated_true_value": {
105
+ "type": "number",
106
+ "description": "The estimated actual value that this is worth",
107
+ },
108
+ "url": {
109
+ "type": "string",
110
+ "description": "The URL of this deal as scraped from the internet",
111
+ },
112
+ },
113
+ "required": ["description", "deal_price", "estimated_true_value", "url"],
114
+ "additionalProperties": False,
115
+ },
116
+ }
117
+
118
+ def get_tools(self):
119
+ """
120
+ Return the json for the tools to be used
121
+ """
122
+ return [
123
+ {"type": "function", "function": self.scan_function},
124
+ {"type": "function", "function": self.estimate_function},
125
+ {"type": "function", "function": self.notify_function},
126
+ ]
127
+
128
+ def handle_tool_call(self, message):
129
+ """
130
+ Actually call the tools associated with this message
131
+ """
132
+ mapping = {
133
+ "scan_the_internet_for_bargains": self.scan_the_internet_for_bargains,
134
+ "estimate_true_value": self.estimate_true_value,
135
+ "notify_user_of_deal": self.notify_user_of_deal,
136
+ }
137
+ results = []
138
+ for tool_call in message.tool_calls:
139
+ tool_name = tool_call.function.name
140
+ arguments = json.loads(tool_call.function.arguments)
141
+ tool = mapping.get(tool_name)
142
+ result = tool(**arguments) if tool else ""
143
+ results.append({"role": "tool", "content": result, "tool_call_id": tool_call.id})
144
+ return results
145
+
146
+ system_message = "You find great deals on bargain products using your tools, and notify the user of the best bargain."
147
+ user_message = """
148
+ First, use your tool to scan the internet for bargain deals. Then for each deal, use your tool to estimate its true value.
149
+ Then pick the single most compelling deal where the price is much lower than the estimated true value, and use your tool to notify the user.
150
+ Then just reply OK to indicate success.
151
+ """
152
+ messages = [
153
+ {"role": "system", "content": system_message},
154
+ {"role": "user", "content": user_message},
155
+ ]
156
+
157
+ def plan(self, memory: List[str] = []) -> Optional[Opportunity]:
158
+ """
159
+ Run the full workflow, providing the LLM with tools to surface scraped deals to the user
160
+ :param memory: a list of URLs that have been surfaced in the past
161
+ :return: an Opportunity if one was surfaced, otherwise None
162
+ """
163
+ self.log("Autonomous Planning Agent is kicking off a run")
164
+ self.memory = memory
165
+ self.opportunity = None
166
+ messages = self.messages[:]
167
+ done = False
168
+ while not done:
169
+ response = self.openai.chat.completions.create(
170
+ model=self.MODEL, messages=messages, tools=self.get_tools()
171
+ )
172
+ if response.choices[0].finish_reason == "tool_calls":
173
+ message = response.choices[0].message
174
+ results = self.handle_tool_call(message)
175
+ messages.append(message)
176
+ messages.extend(results)
177
+ else:
178
+ done = True
179
+ reply = response.choices[0].message.content
180
+ self.log(f"Autonomous Planning Agent completed with: {reply}")
181
+ return self.opportunity
_hf_capstone_repo/agents/deals.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Dict, Self
3
+ from bs4 import BeautifulSoup
4
+ import re
5
+ import feedparser
6
+ from tqdm import tqdm
7
+ import requests
8
+ import time
9
+
10
+ feeds = [
11
+ "https://www.dealnews.com/c142/Electronics/?rss=1",
12
+ "https://www.dealnews.com/c39/Computers/?rss=1",
13
+ "https://www.dealnews.com/f1912/Smart-Home/?rss=1",
14
+ ]
15
+
16
+ # You could also add: "https://www.dealnews.com/c238/Automotive/?rss=1"
17
+ # "https://www.dealnews.com/c196/Home-Garden/?rss=1"
18
+
19
+
20
+ def extract(html_snippet: str) -> str:
21
+ """
22
+ Use Beautiful Soup to clean up this HTML snippet and extract useful text
23
+ """
24
+ soup = BeautifulSoup(html_snippet, "html.parser")
25
+ snippet_div = soup.find("div", class_="snippet summary")
26
+
27
+ if snippet_div:
28
+ description = snippet_div.get_text(strip=True)
29
+ description = BeautifulSoup(description, "html.parser").get_text()
30
+ description = re.sub("<[^<]+?>", "", description)
31
+ result = description.strip()
32
+ else:
33
+ result = html_snippet
34
+ return result.replace("\n", " ")
35
+
36
+
37
+ class ScrapedDeal:
38
+ """
39
+ A class to represent a Deal retrieved from an RSS feed
40
+ """
41
+
42
+ category: str
43
+ title: str
44
+ summary: str
45
+ url: str
46
+ details: str
47
+ features: str
48
+
49
+ def __init__(self, entry: Dict[str, str]):
50
+ """
51
+ Populate this instance based on the provided dict
52
+ """
53
+ self.title = entry["title"]
54
+ self.summary = extract(entry["summary"])
55
+ self.url = entry["links"][0]["href"]
56
+ stuff = requests.get(self.url).content
57
+ soup = BeautifulSoup(stuff, "html.parser")
58
+ content = soup.find("div", class_="content-section").get_text()
59
+ content = content.replace("\nmore", "").replace("\n", " ")
60
+ if "Features" in content:
61
+ self.details, self.features = content.split("Features", 1)
62
+ else:
63
+ self.details = content
64
+ self.features = ""
65
+ self.truncate()
66
+
67
+ def truncate(self):
68
+ """
69
+ Limit the fields to a sensible length to avoid sending too much info to the model
70
+ """
71
+ self.title = self.title[:100]
72
+ self.details = self.details[:500]
73
+ self.features = self.features[:500]
74
+
75
+ def __repr__(self):
76
+ """
77
+ Return a string to describe this deal
78
+ """
79
+ return f"<{self.title}>"
80
+
81
+ def describe(self):
82
+ """
83
+ Return a longer string to describe this deal for use in calling a model
84
+ """
85
+ return f"Title: {self.title}\nDetails: {self.details.strip()}\nFeatures: {self.features.strip()}\nURL: {self.url}"
86
+
87
+ @classmethod
88
+ def fetch(cls, show_progress: bool = False) -> List[Self]:
89
+ """
90
+ Retrieve all deals from the selected RSS feeds
91
+ """
92
+ deals = []
93
+ feed_iter = tqdm(feeds) if show_progress else feeds
94
+ for feed_url in feed_iter:
95
+ feed = feedparser.parse(feed_url)
96
+ for entry in feed.entries[:10]:
97
+ deals.append(cls(entry))
98
+ time.sleep(0.05)
99
+ return deals
100
+
101
+
102
+ class Deal(BaseModel):
103
+ """
104
+ A class to Represent a Deal with a summary description
105
+ """
106
+
107
+ product_description: str = Field(
108
+ description="Your clearly expressed summary of the product in 3-4 sentences. Details of the item are much more important than why it's a good deal. Avoid mentioning discounts and coupons; focus on the item itself. There should be a short paragraph of text for each item you choose."
109
+ )
110
+ price: float = Field(
111
+ description="The actual price of this product, as advertised in the deal. Be sure to give the actual price; for example, if a deal is described as $100 off the usual $300 price, you should respond with $200"
112
+ )
113
+ url: str = Field(description="The URL of the deal, as provided in the input")
114
+
115
+
116
+ class DealSelection(BaseModel):
117
+ """
118
+ A class to Represent a list of Deals
119
+ """
120
+
121
+ deals: List[Deal] = Field(
122
+ description="Your selection of the 5 deals that have the most detailed, high quality description and the most clear price. You should be confident that the price reflects the deal, that it is a good deal, with a clear description"
123
+ )
124
+
125
+
126
+ class Opportunity(BaseModel):
127
+ """
128
+ A class to represent a possible opportunity: a Deal where we estimate
129
+ it should cost more than it's being offered
130
+ """
131
+
132
+ deal: Deal
133
+ estimate: float
134
+ discount: float
_hf_capstone_repo/agents/deep_neural_network.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from tqdm.notebook import tqdm
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.optim as optim
6
+ from torch.utils.data import DataLoader, TensorDataset
7
+ from torch.optim.lr_scheduler import CosineAnnealingLR
8
+ from sklearn.feature_extraction.text import HashingVectorizer
9
+ import logging
10
+
11
+
12
+ class ResidualBlock(nn.Module):
13
+ def __init__(self, hidden_size, dropout_prob):
14
+ super(ResidualBlock, self).__init__()
15
+ self.block = nn.Sequential(
16
+ nn.Linear(hidden_size, hidden_size),
17
+ nn.LayerNorm(hidden_size),
18
+ nn.ReLU(),
19
+ nn.Dropout(dropout_prob),
20
+ nn.Linear(hidden_size, hidden_size),
21
+ nn.LayerNorm(hidden_size),
22
+ )
23
+ self.relu = nn.ReLU()
24
+
25
+ def forward(self, x):
26
+ residual = x
27
+ out = self.block(x)
28
+ out += residual # Skip connection
29
+ return self.relu(out)
30
+
31
+
32
+ class DeepNeuralNetwork(nn.Module):
33
+ def __init__(self, input_size, num_layers=10, hidden_size=4096, dropout_prob=0.2):
34
+ super(DeepNeuralNetwork, self).__init__()
35
+
36
+ # First layer
37
+ self.input_layer = nn.Sequential(
38
+ nn.Linear(input_size, hidden_size),
39
+ nn.LayerNorm(hidden_size),
40
+ nn.ReLU(),
41
+ nn.Dropout(dropout_prob),
42
+ )
43
+
44
+ # Residual blocks
45
+ self.residual_blocks = nn.ModuleList()
46
+ for i in range(num_layers - 2):
47
+ self.residual_blocks.append(ResidualBlock(hidden_size, dropout_prob))
48
+
49
+ # Output layer
50
+ self.output_layer = nn.Linear(hidden_size, 1)
51
+
52
+ def forward(self, x):
53
+ x = self.input_layer(x)
54
+
55
+ for block in self.residual_blocks:
56
+ x = block(x)
57
+
58
+ return self.output_layer(x)
59
+
60
+
61
+ Y_STD = 1.0328539609909058
62
+ Y_MEAN = 4.434937953948975
63
+
64
+
65
+ class DeepNeuralNetworkInference:
66
+ def __init__(self):
67
+ self.vectorizer = None
68
+ self.model = None
69
+ self.device = None
70
+
71
+ np.random.seed(42)
72
+ torch.manual_seed(42)
73
+ torch.cuda.manual_seed(42)
74
+
75
+ def setup(self):
76
+ self.vectorizer = HashingVectorizer(n_features=5000, stop_words="english", binary=True)
77
+ self.model = DeepNeuralNetwork(5000)
78
+ if torch.cuda.is_available():
79
+ self.device = torch.device("cuda")
80
+ elif torch.backends.mps.is_available():
81
+ self.device = torch.device("mps")
82
+ else:
83
+ self.device = torch.device("cpu")
84
+
85
+ logging.info(f"Neural Network is using {self.device}")
86
+
87
+ self.model.to(self.device)
88
+
89
+ def load(self, path):
90
+ self.model.load_state_dict(torch.load(path, map_location=self.device))
91
+ self.model.to(self.device)
92
+
93
+ def inference(self, text):
94
+ self.model.eval()
95
+ with torch.no_grad():
96
+ vector = self.vectorizer.transform([text])
97
+ vector = torch.FloatTensor(vector.toarray()).to(self.device)
98
+ pred = self.model(vector)[0]
99
+ result = torch.exp(pred * Y_STD + Y_MEAN) - 1
100
+ result = result.item()
101
+ return max(0, result)
_hf_capstone_repo/agents/ensemble_agent.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.agent import Agent
2
+ from agents.specialist_agent import SpecialistAgent
3
+ from agents.frontier_agent import FrontierAgent
4
+ from agents.neural_network_agent import NeuralNetworkAgent
5
+ from agents.preprocessor import Preprocessor
6
+ from free_config import WEIGHTS_WITH_SPECIALIST, WEIGHTS_WITHOUT_SPECIALIST
7
+
8
+
9
+ class EnsembleAgent(Agent):
10
+ name = "Ensemble Agent"
11
+ color = Agent.YELLOW
12
+
13
+ def __init__(self, collection):
14
+ """
15
+ Create an instance of Ensemble, by creating each of the models
16
+ And loading the weights of the Ensemble
17
+ """
18
+ self.log("Initializing Ensemble Agent")
19
+ self.specialist = SpecialistAgent()
20
+ self.frontier = FrontierAgent(collection)
21
+ self.neural_network = NeuralNetworkAgent()
22
+ self.preprocessor = Preprocessor()
23
+ self.use_specialist = self.specialist.available
24
+ if self.use_specialist:
25
+ mode = getattr(self.specialist, "mode", "on")
26
+ self.log(f"Ensemble Agent is ready (Frontier + Specialist [{mode}] + Neural Network)")
27
+ else:
28
+ self.log("Ensemble Agent is ready (Frontier + Neural Network only)")
29
+
30
+ def price(self, description: str) -> float:
31
+ """
32
+ Run this ensemble model
33
+ Ask each of the models to price the product
34
+ Then return the weighted average price
35
+ """
36
+ self.log("Running Ensemble Agent - preprocessing text")
37
+ rewrite = self.preprocessor.preprocess(description)
38
+ self.log(f"Pre-processed text using {self.preprocessor.model_name}")
39
+ frontier = self.frontier.price(rewrite)
40
+ neural_network = self.neural_network.price(rewrite)
41
+
42
+ if self.use_specialist:
43
+ specialist = self.specialist.price(rewrite)
44
+ wf, ws, wn = WEIGHTS_WITH_SPECIALIST
45
+ combined = frontier * wf + specialist * ws + neural_network * wn
46
+ else:
47
+ wf, wn = WEIGHTS_WITHOUT_SPECIALIST
48
+ combined = frontier * wf + neural_network * wn
49
+
50
+ self.log(f"Ensemble Agent complete - returning ${combined:.2f}")
51
+ return combined
_hf_capstone_repo/agents/evaluator.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from sklearn.metrics import mean_squared_error, r2_score
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from itertools import accumulate
7
+ import math
8
+ from tqdm.notebook import tqdm
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ GREEN = "\033[92m"
12
+ YELLOW = "\033[93m"
13
+ RED = "\033[91m"
14
+ RESET = "\033[0m"
15
+ COLOR_MAP = {"red": RED, "orange": YELLOW, "green": GREEN}
16
+
17
+ WORKERS = 5
18
+ DEFAULT_SIZE = 200
19
+
20
+
21
+ class Tester:
22
+ def __init__(self, predictor, data, title=None, size=DEFAULT_SIZE, workers=WORKERS):
23
+ self.predictor = predictor
24
+ self.data = data
25
+ self.title = title or self.make_title(predictor)
26
+ self.size = size
27
+ self.titles = []
28
+ self.guesses = []
29
+ self.truths = []
30
+ self.errors = []
31
+ self.colors = []
32
+ self.workers = workers
33
+
34
+ @staticmethod
35
+ def make_title(predictor) -> str:
36
+ return predictor.__name__.replace("__", ".").replace("_", " ").title().replace("Gpt", "GPT")
37
+
38
+ @staticmethod
39
+ def post_process(value):
40
+ if isinstance(value, str):
41
+ value = value.replace("$", "").replace(",", "")
42
+ match = re.search(r"[-+]?\d*\.\d+|\d+", value)
43
+ return float(match.group()) if match else 0
44
+ else:
45
+ return value
46
+
47
+ def color_for(self, error, truth):
48
+ if error < 40 or error / truth < 0.2:
49
+ return "green"
50
+ elif error < 80 or error / truth < 0.4:
51
+ return "orange"
52
+ else:
53
+ return "red"
54
+
55
+ def run_datapoint(self, i):
56
+ datapoint = self.data[i]
57
+ value = self.predictor(datapoint)
58
+ guess = self.post_process(value)
59
+ truth = datapoint.price
60
+ error = abs(guess - truth)
61
+ color = self.color_for(error, truth)
62
+ title = datapoint.title if len(datapoint.title) <= 40 else datapoint.title[:40] + "..."
63
+ return title, guess, truth, error, color
64
+
65
+ def chart(self, title):
66
+ df = pd.DataFrame(
67
+ {
68
+ "truth": self.truths,
69
+ "guess": self.guesses,
70
+ "title": self.titles,
71
+ "error": self.errors,
72
+ "color": self.colors,
73
+ }
74
+ )
75
+
76
+ # Pre-format hover text
77
+ df["hover"] = [
78
+ f"{t}\nGuess=${g:,.2f} Actual=${y:,.2f}"
79
+ for t, g, y in zip(df["title"], df["guess"], df["truth"])
80
+ ]
81
+
82
+ max_val = float(max(df["truth"].max(), df["guess"].max()))
83
+
84
+ fig = px.scatter(
85
+ df,
86
+ x="truth",
87
+ y="guess",
88
+ color="color",
89
+ color_discrete_map={"green": "green", "orange": "orange", "red": "red"},
90
+ title=title,
91
+ labels={"truth": "Actual Price", "guess": "Predicted Price"},
92
+ width=1000,
93
+ height=800,
94
+ )
95
+
96
+ # Assign customdata per trace (one color/category = one trace)
97
+ for tr in fig.data:
98
+ mask = df["color"] == tr.name
99
+ tr.customdata = df.loc[mask, ["hover"]].to_numpy()
100
+ tr.hovertemplate = "%{customdata[0]}<extra></extra>"
101
+ tr.marker.update(size=6)
102
+
103
+ # Reference line y=x
104
+ fig.add_trace(
105
+ go.Scatter(
106
+ x=[0, max_val],
107
+ y=[0, max_val],
108
+ mode="lines",
109
+ line=dict(width=2, dash="dash", color="deepskyblue"),
110
+ name="y = x",
111
+ hoverinfo="skip",
112
+ showlegend=False,
113
+ )
114
+ )
115
+
116
+ fig.update_xaxes(range=[0, max_val])
117
+ fig.update_yaxes(range=[0, max_val])
118
+ fig.update_layout(showlegend=False)
119
+ fig.show()
120
+
121
+ def error_trend_chart(self):
122
+ n = len(self.errors)
123
+
124
+ # Running mean and std (pure Python)
125
+ running_sums = list(accumulate(self.errors))
126
+ x = list(range(1, n + 1))
127
+ running_means = [s / i for s, i in zip(running_sums, x)]
128
+
129
+ running_squares = list(accumulate(e * e for e in self.errors))
130
+ running_stds = [
131
+ math.sqrt((sq_sum / i) - (mean**2)) if i > 1 else 0
132
+ for i, sq_sum, mean in zip(x, running_squares, running_means)
133
+ ]
134
+
135
+ # 95% confidence interval for mean
136
+ ci = [1.96 * (sd / math.sqrt(i)) if i > 1 else 0 for i, sd in zip(x, running_stds)]
137
+ upper = [m + c for m, c in zip(running_means, ci)]
138
+ lower = [m - c for m, c in zip(running_means, ci)]
139
+
140
+ # Plot
141
+ fig = go.Figure()
142
+
143
+ # Shaded confidence interval band
144
+ fig.add_trace(
145
+ go.Scatter(
146
+ x=x + x[::-1],
147
+ y=upper + lower[::-1],
148
+ fill="toself",
149
+ fillcolor="rgba(128,128,128,0.2)",
150
+ line=dict(color="rgba(255,255,255,0)"),
151
+ hoverinfo="skip",
152
+ showlegend=False,
153
+ name="95% CI",
154
+ )
155
+ )
156
+
157
+ # Main line with hover text showing CI
158
+ fig.add_trace(
159
+ go.Scatter(
160
+ x=x,
161
+ y=running_means,
162
+ mode="lines",
163
+ line=dict(width=3, color="firebrick"),
164
+ name="Cumulative Avg Error",
165
+ customdata=list(
166
+ zip(
167
+ ci,
168
+ )
169
+ ),
170
+ hovertemplate=(
171
+ "n=%{x}<br>"
172
+ "Avg Error=$%{y:,.2f}<br>"
173
+ "±95% CI=$%{customdata[0]:,.2f}<extra></extra>"
174
+ ),
175
+ )
176
+ )
177
+
178
+ # Title with final stats
179
+ final_mean = running_means[-1]
180
+ final_ci = ci[-1]
181
+ title = f"{self.title} Error: ${final_mean:,.2f} ± ${final_ci:,.2f}"
182
+
183
+ fig.update_layout(
184
+ title=title,
185
+ xaxis_title="Number of Datapoints",
186
+ yaxis_title="Average Absolute Error ($)",
187
+ width=1000,
188
+ height=360,
189
+ template="plotly_white",
190
+ showlegend=False,
191
+ )
192
+
193
+ fig.show()
194
+
195
+ def report(self):
196
+ average_error = sum(self.errors) / self.size
197
+ mse = mean_squared_error(self.truths, self.guesses)
198
+ r2 = r2_score(self.truths, self.guesses) * 100
199
+ title = f"{self.title} results<br><b>Error:</b> ${average_error:,.2f} <b>MSE:</b> {mse:,.0f} <b>r²:</b> {r2:.1f}%"
200
+ self.error_trend_chart()
201
+ self.chart(title)
202
+
203
+ def run(self):
204
+ with ThreadPoolExecutor(max_workers=self.workers) as ex:
205
+ for title, guess, truth, error, color in tqdm(
206
+ ex.map(self.run_datapoint, range(self.size)), total=self.size
207
+ ):
208
+ self.titles.append(title)
209
+ self.guesses.append(guess)
210
+ self.truths.append(truth)
211
+ self.errors.append(error)
212
+ self.colors.append(color)
213
+ print(f"{COLOR_MAP[color]}${error:.0f} ", end="")
214
+ self.report()
215
+
216
+
217
+ def evaluate(function, data, size=DEFAULT_SIZE, workers=WORKERS):
218
+ Tester(function, data, size=size, workers=workers).run()
_hf_capstone_repo/agents/frontier_agent.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from typing import List, Dict
4
+
5
+ from groq import Groq
6
+ from sentence_transformers import SentenceTransformer
7
+
8
+ from agents.agent import Agent
9
+ from free_config import GROQ_MODEL
10
+
11
+
12
+ class FrontierAgent(Agent):
13
+ name = "Frontier Agent"
14
+ color = Agent.BLUE
15
+
16
+ def __init__(self, collection):
17
+ """
18
+ Set up this instance by connecting to Groq, the Chroma datastore,
19
+ and the local sentence-transformer embedding model.
20
+ """
21
+ self.log("Initializing Frontier Agent")
22
+ self.client = Groq(api_key=os.environ["GROQ_API_KEY"])
23
+ self.model = GROQ_MODEL
24
+ self.log(f"Frontier Agent is setting up with Groq ({self.model})")
25
+ self.collection = collection
26
+ self.encoder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
27
+ self.log("Frontier Agent is ready")
28
+
29
+ def make_context(self, similars: List[str], prices: List[float]) -> str:
30
+ """
31
+ Create context that can be inserted into the prompt
32
+ """
33
+ message = "To provide some context, here are some other items that might be similar to the item you need to estimate.\n\n"
34
+ for similar, price in zip(similars, prices):
35
+ message += f"Potentially related product:\n{similar}\nPrice is ${price:.2f}\n\n"
36
+ return message
37
+
38
+ def messages_for(
39
+ self, description: str, similars: List[str], prices: List[float]
40
+ ) -> List[Dict[str, str]]:
41
+ """
42
+ Create the message list for the Groq chat completion call
43
+ """
44
+ message = f"Estimate the price of this product. Respond with the price only, no explanation.\n\n{description}\n\n"
45
+ message += self.make_context(similars, prices)
46
+ return [{"role": "user", "content": message}]
47
+
48
+ def find_similars(self, description: str):
49
+ """
50
+ Return a list of items similar to the given one by looking in the Chroma datastore
51
+ """
52
+ self.log(
53
+ "Frontier Agent is performing a RAG search of the Chroma datastore to find 5 similar products"
54
+ )
55
+ vector = self.encoder.encode([description])
56
+ results = self.collection.query(query_embeddings=vector.astype(float).tolist(), n_results=5)
57
+ documents = results["documents"][0][:]
58
+ prices = [m["price"] for m in results["metadatas"][0][:]]
59
+ self.log("Frontier Agent has found similar products")
60
+ return documents, prices
61
+
62
+ def get_price(self, s) -> float:
63
+ """
64
+ A utility that plucks a floating point number out of a string
65
+ """
66
+ s = s.replace("$", "").replace(",", "")
67
+ match = re.search(r"[-+]?\d*\.\d+|\d+", s)
68
+ return float(match.group()) if match else 0.0
69
+
70
+ def price(self, description: str) -> float:
71
+ """
72
+ Call Groq to estimate the price using RAG context from similar products.
73
+ """
74
+ documents, prices = self.find_similars(description)
75
+ self.log(
76
+ f"Frontier Agent is about to call Groq ({self.model}) with context including 5 similar products"
77
+ )
78
+ response = self.client.chat.completions.create(
79
+ model=self.model,
80
+ messages=self.messages_for(description, documents, prices),
81
+ temperature=0,
82
+ )
83
+ reply = response.choices[0].message.content
84
+ result = self.get_price(reply)
85
+ self.log(f"Frontier Agent completed - predicting ${result:.2f}")
86
+ return result
_hf_capstone_repo/agents/items.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from datasets import Dataset, DatasetDict, load_dataset
3
+ from typing import Optional, Self
4
+
5
+
6
+ PREFIX = "Price is $"
7
+ QUESTION = "What does this cost to the nearest dollar?"
8
+
9
+
10
+ class Item(BaseModel):
11
+ """
12
+ An Item is a data-point of a Product with a Price
13
+ """
14
+
15
+ title: str
16
+ category: str
17
+ price: float
18
+ full: Optional[str] = None
19
+ weight: Optional[float] = None
20
+ summary: Optional[str] = None
21
+ prompt: Optional[str] = None
22
+ id: Optional[int] = None
23
+
24
+ def make_prompt(self, text: str):
25
+ self.prompt = f"{QUESTION}\n\n{text}\n\n{PREFIX}{round(self.price)}.00"
26
+
27
+ def test_prompt(self) -> str:
28
+ return self.prompt.split(PREFIX)[0] + PREFIX
29
+
30
+ def __repr__(self) -> str:
31
+ return f"<{self.title} = ${self.price}>"
32
+
33
+ @staticmethod
34
+ def push_to_hub(dataset_name: str, train: list[Self], val: list[Self], test: list[Self]):
35
+ """Push Item lists to HuggingFace Hub"""
36
+ DatasetDict(
37
+ {
38
+ "train": Dataset.from_list([item.model_dump() for item in train]),
39
+ "validation": Dataset.from_list([item.model_dump() for item in val]),
40
+ "test": Dataset.from_list([item.model_dump() for item in test]),
41
+ }
42
+ ).push_to_hub(dataset_name)
43
+
44
+ @classmethod
45
+ def from_hub(cls, dataset_name: str) -> tuple[list[Self], list[Self], list[Self]]:
46
+ """Load from HuggingFace Hub and reconstruct Items"""
47
+ ds = load_dataset(dataset_name)
48
+ return (
49
+ [cls.model_validate(row) for row in ds["train"]],
50
+ [cls.model_validate(row) for row in ds["validation"]],
51
+ [cls.model_validate(row) for row in ds["test"]],
52
+ )
_hf_capstone_repo/agents/messaging_agent.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import requests
4
+ from litellm import completion
5
+
6
+ from agents.deals import Opportunity
7
+ from agents.agent import Agent
8
+ from free_config import MESSAGING_MODEL, PUSHOVER_CONFIGURED
9
+
10
+ pushover_url = "https://api.pushover.net/1/messages.json"
11
+
12
+
13
+ class MessagingAgent(Agent):
14
+ name = "Messaging Agent"
15
+ color = Agent.WHITE
16
+
17
+ def __init__(self):
18
+ """
19
+ Set up Pushover notifications (optional) and Groq for message crafting.
20
+ """
21
+ self.log("Messaging Agent is initializing")
22
+ self.pushover_user = os.getenv("PUSHOVER_USER", "")
23
+ self.pushover_token = os.getenv("PUSHOVER_TOKEN", "")
24
+ self.model = MESSAGING_MODEL
25
+ if PUSHOVER_CONFIGURED:
26
+ self.log("Messaging Agent has Pushover configured")
27
+ else:
28
+ self.log("Messaging Agent: Pushover not configured — alerts will be logged only")
29
+
30
+ def push(self, text):
31
+ """
32
+ Send a Push Notification using the Pushover API, or log if not configured.
33
+ """
34
+ if not PUSHOVER_CONFIGURED:
35
+ self.log(f"Deal alert (Pushover skipped): {text[:120]}...")
36
+ return
37
+ self.log("Messaging Agent is sending a push notification")
38
+ payload = {
39
+ "user": self.pushover_user,
40
+ "token": self.pushover_token,
41
+ "message": text,
42
+ "sound": "cashregister",
43
+ }
44
+ requests.post(pushover_url, data=payload)
45
+
46
+ def alert(self, opportunity: Opportunity):
47
+ """
48
+ Make an alert about the specified Opportunity
49
+ """
50
+ text = f"Deal Alert! Price=${opportunity.deal.price:.2f}, "
51
+ text += f"Estimate=${opportunity.estimate:.2f}, "
52
+ text += f"Discount=${opportunity.discount:.2f} :"
53
+ text += opportunity.deal.product_description[:10] + "... "
54
+ text += opportunity.deal.url
55
+ self.push(text)
56
+ self.log("Messaging Agent has completed")
57
+
58
+ def craft_message(
59
+ self, description: str, deal_price: float, estimated_true_value: float
60
+ ) -> str:
61
+ user_prompt = "Please summarize this great deal in 2-3 sentences to be sent as an exciting push notification alerting the user about this deal.\n"
62
+ user_prompt += f"Item Description: {description}\nOffered Price: {deal_price}\nEstimated true value: {estimated_true_value}"
63
+ user_prompt += "\n\nRespond only with the 2-3 sentence message which will be used to alert & excite the user about this deal"
64
+ response = completion(
65
+ model=self.model,
66
+ messages=[
67
+ {"role": "user", "content": user_prompt},
68
+ ],
69
+ )
70
+ return response.choices[0].message.content
71
+
72
+ def notify(self, description: str, deal_price: float, estimated_true_value: float, url: str):
73
+ """
74
+ Make an alert about the specified details
75
+ """
76
+ self.log(f"Messaging Agent is using Groq ({self.model}) to craft the message")
77
+ text = self.craft_message(description, deal_price, estimated_true_value)
78
+ self.push(text[:200] + "... " + url)
79
+ self.log("Messaging Agent has completed")
_hf_capstone_repo/agents/neural_network_agent.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.agent import Agent
2
+ from agents.deep_neural_network import DeepNeuralNetworkInference
3
+
4
+
5
+ class NeuralNetworkAgent(Agent):
6
+ name = "Neural Network Agent"
7
+ color = Agent.MAGENTA
8
+
9
+ def __init__(self):
10
+ """
11
+ Initialize this object by loading in the saved model weights
12
+ and the SentenceTransformer vector encoding model
13
+ """
14
+ self.log("Neural Network Agent is initializing")
15
+ self.neural_network = DeepNeuralNetworkInference()
16
+ self.neural_network.setup()
17
+ self.neural_network.load("deep_neural_network.pth")
18
+ self.log("Neural Network Agent is ready and weights are loaded")
19
+
20
+ def price(self, description: str) -> float:
21
+ """
22
+ Use the Deep Neural Network to estimate the price of the described item
23
+ :param description: the product to be estimated
24
+ :return: the price as a float
25
+ """
26
+ self.log("Neural Network Agent is starting a prediction")
27
+ result = self.neural_network.inference(description)
28
+ self.log(f"Neural Network Agent completed - predicting ${result:.2f}")
29
+ return result
_hf_capstone_repo/agents/planning_agent.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List
2
+ from agents.agent import Agent
3
+ from agents.deals import ScrapedDeal, DealSelection, Deal, Opportunity
4
+ from agents.scanner_agent import ScannerAgent
5
+ from agents.ensemble_agent import EnsembleAgent
6
+ from agents.messaging_agent import MessagingAgent
7
+
8
+
9
+ class PlanningAgent(Agent):
10
+
11
+ name = "Planning Agent"
12
+ color = Agent.GREEN
13
+ DEAL_THRESHOLD = 50
14
+
15
+ def __init__(self, collection):
16
+ """
17
+ Create instances of the 3 Agents that this planner coordinates across
18
+ """
19
+ self.log("Planning Agent is initializing")
20
+ self.scanner = ScannerAgent()
21
+ self.ensemble = EnsembleAgent(collection)
22
+ self.messenger = MessagingAgent()
23
+ self.log("Planning Agent is ready")
24
+
25
+ def run(self, deal: Deal) -> Opportunity:
26
+ """
27
+ Run the workflow for a particular deal
28
+ :param deal: the deal, summarized from an RSS scrape
29
+ :returns: an opportunity including the discount
30
+ """
31
+ self.log("Planning Agent is pricing up a potential deal")
32
+ estimate = self.ensemble.price(deal.product_description)
33
+ discount = estimate - deal.price
34
+ self.log(f"Planning Agent has processed a deal with discount ${discount:.2f}")
35
+ return Opportunity(deal=deal, estimate=estimate, discount=discount)
36
+
37
+ def plan(self, memory: List[str] = []) -> Optional[Opportunity]:
38
+ """
39
+ Run the full workflow:
40
+ 1. Use the ScannerAgent to find deals from RSS feeds
41
+ 2. Use the EnsembleAgent to estimate them
42
+ 3. Use the MessagingAgent to send a notification of deals
43
+ :param memory: a list of URLs that have been surfaced in the past
44
+ :return: an Opportunity if one was surfaced, otherwise None
45
+ """
46
+ self.log("Planning Agent is kicking off a run")
47
+ selection = self.scanner.scan(memory=memory)
48
+ if selection:
49
+ opportunities = [self.run(deal) for deal in selection.deals[:5]]
50
+ opportunities.sort(key=lambda opp: opp.discount, reverse=True)
51
+ best = opportunities[0]
52
+ self.log(f"Planning Agent has identified the best deal has discount ${best.discount:.2f}")
53
+ if best.discount > self.DEAL_THRESHOLD:
54
+ self.messenger.alert(best)
55
+ self.log("Planning Agent has completed a run")
56
+ return best if best.discount > self.DEAL_THRESHOLD else None
57
+ return None
_hf_capstone_repo/agents/preprocessor.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+ from litellm import completion
5
+
6
+ from free_config import PREPROCESSOR_MODEL
7
+
8
+ load_dotenv(override=True)
9
+
10
+ DEFAULT_REASONING_EFFORT = "low" if "gpt-oss" in PREPROCESSOR_MODEL else None
11
+
12
+ SYSTEM_PROMPT = """Create a concise description of a product. Respond only in this format. Do not include part numbers.
13
+ Title: Rewritten short precise title
14
+ Category: eg Electronics
15
+ Brand: Brand name
16
+ Description: 1 sentence description
17
+ Details: 1 sentence on features"""
18
+
19
+
20
+ class Preprocessor:
21
+ def __init__(
22
+ self,
23
+ model_name=PREPROCESSOR_MODEL,
24
+ reasoning_effort=DEFAULT_REASONING_EFFORT,
25
+ base_url=None,
26
+ ):
27
+ self.total_input_tokens = 0
28
+ self.total_output_tokens = 0
29
+ self.total_cost = 0
30
+ self.model_name = model_name
31
+ self.reasoning_effort = reasoning_effort
32
+ self.base_url = base_url
33
+ if "ollama" in model_name and not base_url:
34
+ self.base_url = "http://localhost:11434"
35
+
36
+ def messages_for(self, text: str) -> list[dict]:
37
+ return [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": text}]
38
+
39
+ def preprocess(self, text: str) -> str:
40
+ messages = self.messages_for(text)
41
+ response = completion(
42
+ messages=messages,
43
+ model=self.model_name,
44
+ reasoning_effort=self.reasoning_effort,
45
+ api_base=self.base_url,
46
+ )
47
+ self.total_input_tokens += response.usage.prompt_tokens
48
+ self.total_output_tokens += response.usage.completion_tokens
49
+ self.total_cost += response._hidden_params["response_cost"]
50
+ return response.choices[0].message.content
_hf_capstone_repo/agents/scanner_agent.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from typing import Optional, List
5
+
6
+ from groq import BadRequestError, Groq
7
+
8
+ from agents.deals import ScrapedDeal, DealSelection
9
+ from agents.agent import Agent
10
+ from free_config import GROQ_MODEL
11
+
12
+
13
+ class ScannerAgent(Agent):
14
+ MAX_DEALS_PER_REQUEST = 15
15
+
16
+ JSON_FORMAT_HINT = (
17
+ '\n\nRespond with JSON only, using this shape: '
18
+ '{"deals": [{"product_description": "...", "price": 99.99, "url": "https://..."}]}'
19
+ '\nThe JSON must be valid: no double-quote characters inside product_description '
20
+ '(write "6.9 inches" instead of 6.9"). No trailing commas. '
21
+ 'Close the deals array with a single ] and the object with a single }.'
22
+ )
23
+
24
+ SYSTEM_PROMPT = """You identify and summarize the 5 most detailed deals from a list, by selecting deals that have the most detailed, high quality description and the most clear price.
25
+ You should provide the price as a number derived from the description. If the price of a deal isn't clear, do not include that deal in your response.
26
+ Most important is that you respond with the 5 deals that have the most detailed product description with price. It's not important to mention the terms of the deal; most important is a thorough description of the product.
27
+ Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price.
28
+ Never use the double-quote character inside product_description text. Spell out inch measurements as words, e.g. "55 inches" not 55".
29
+ """
30
+
31
+ USER_PROMPT_PREFIX = """Respond with the most promising 5 deals from this list, selecting those which have the most detailed, high quality product description and a clear price that is greater than 0.
32
+ You should rephrase the description to be a summary of the product itself, not the terms of the deal.
33
+ Remember to respond with a short paragraph of text in the product_description field for each of the 5 items that you select.
34
+ Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price.
35
+
36
+ Deals:
37
+
38
+ """
39
+
40
+ USER_PROMPT_SUFFIX = "\n\nInclude exactly 5 deals, no more."
41
+
42
+ name = "Scanner Agent"
43
+ color = Agent.CYAN
44
+
45
+ def __init__(self):
46
+ self.log("Scanner Agent is initializing")
47
+ self.client = Groq(api_key=os.environ["GROQ_API_KEY"])
48
+ self.model = GROQ_MODEL
49
+ self.log(f"Scanner Agent is ready (Groq / {self.model})")
50
+
51
+ def fetch_deals(self, memory) -> List[ScrapedDeal]:
52
+ """
53
+ Look up deals published on RSS feeds
54
+ Return any new deals that are not already in the memory provided
55
+ """
56
+ self.log("Scanner Agent is about to fetch deals from RSS feed")
57
+ urls = [opp.deal.url for opp in memory]
58
+ scraped = ScrapedDeal.fetch()
59
+ result = [scrape for scrape in scraped if scrape.url not in urls]
60
+ self.log(f"Scanner Agent received {len(result)} deals not already scraped")
61
+ return result
62
+
63
+ def make_user_prompt(self, scraped) -> str:
64
+ """
65
+ Create a user prompt based on the scraped deals provided
66
+ """
67
+ user_prompt = self.USER_PROMPT_PREFIX
68
+ user_prompt += "\n\n".join(
69
+ [scrape.describe() for scrape in scraped[: self.MAX_DEALS_PER_REQUEST]]
70
+ )
71
+ user_prompt += self.USER_PROMPT_SUFFIX
72
+ return user_prompt
73
+
74
+ @staticmethod
75
+ def _repair_deals_json(text: str) -> str:
76
+ text = text.strip()
77
+ text = re.sub(r'(\d+(?:\.\d+)?)"(\s+(?=[a-zA-Z0-9]))', r"\1 inches\2", text)
78
+ text = re.sub(r'("url"\s*:\s*"[^"]*")\s*\]', r"\1\n }", text)
79
+ text = re.sub(
80
+ r'("url"\s*:\s*"[^"]*")\s*\}\s*\n\s*\]\s*\n\s*\}\}',
81
+ r"\1\n }\n ]\n}",
82
+ text,
83
+ )
84
+ text = re.sub(r"(\])\s*\]\s*\}\s*\}", r"\1}", text)
85
+ text = re.sub(r"(\])\s*\]\s*\}", r"\1}", text)
86
+ text = re.sub(r"\}\s*\}\s*$", r"}", text)
87
+ return text
88
+
89
+ def _parse_deals_json(self, content: str) -> DealSelection:
90
+ last_error = None
91
+ for candidate in (content, self._repair_deals_json(content)):
92
+ try:
93
+ result = DealSelection.model_validate(json.loads(candidate))
94
+ result.deals = [deal for deal in result.deals if deal.price > 0]
95
+ return result
96
+ except (json.JSONDecodeError, ValueError) as exc:
97
+ last_error = exc
98
+ raise ValueError(f"Could not parse deals JSON: {last_error}")
99
+
100
+ @staticmethod
101
+ def _extract_failed_generation(exc: BadRequestError) -> Optional[str]:
102
+ body = getattr(exc, "body", None)
103
+ if isinstance(body, dict):
104
+ return body.get("error", {}).get("failed_generation")
105
+ return None
106
+
107
+ def _call_groq(self, user_prompt: str) -> DealSelection:
108
+ messages = [
109
+ {"role": "system", "content": self.SYSTEM_PROMPT + self.JSON_FORMAT_HINT},
110
+ {"role": "user", "content": user_prompt},
111
+ ]
112
+ try:
113
+ response = self.client.chat.completions.create(
114
+ model=self.model,
115
+ messages=messages,
116
+ response_format={"type": "json_object"},
117
+ temperature=0,
118
+ )
119
+ return self._parse_deals_json(response.choices[0].message.content)
120
+ except BadRequestError as exc:
121
+ failed = self._extract_failed_generation(exc)
122
+ if failed:
123
+ self.log("Scanner Agent repairing malformed JSON from Groq response")
124
+ return self._parse_deals_json(failed)
125
+ raise
126
+
127
+ def scan(self, memory: List[str] = []) -> Optional[DealSelection]:
128
+ """
129
+ Call Groq to provide a high potential list of deals with good descriptions and prices
130
+ :param memory: a list of URLs representing deals already raised
131
+ :return: a selection of good deals, or None if there aren't any
132
+ """
133
+ scraped = self.fetch_deals(memory)
134
+ if not scraped:
135
+ return None
136
+ user_prompt = self.make_user_prompt(scraped)
137
+ self.log(f"Scanner Agent is calling Groq ({self.model})")
138
+ try:
139
+ result = self._call_groq(user_prompt)
140
+ except Exception as exc:
141
+ self.log(f"Scanner Agent failed to parse Groq response: {exc}")
142
+ return None
143
+ self.log(
144
+ f"Scanner Agent received {len(result.deals)} selected deals with price>0 from Groq"
145
+ )
146
+ return result
147
+
148
+ def test_scan(self, memory: List[str] = []) -> Optional[DealSelection]:
149
+ """
150
+ Return a test DealSelection, to be used during testing
151
+ """
152
+ results = {
153
+ "deals": [
154
+ {
155
+ "product_description": "The Hisense R6 Series 55R6030N is a 55-inch 4K UHD Roku Smart TV that offers stunning picture quality with 3840x2160 resolution. It features Dolby Vision HDR and HDR10 compatibility, ensuring a vibrant and dynamic viewing experience. The TV runs on Roku's operating system, allowing easy access to streaming services and voice control compatibility with Google Assistant and Alexa. With three HDMI ports available, connecting multiple devices is simple and efficient.",
156
+ "price": 178,
157
+ "url": "https://www.dealnews.com/products/Hisense/Hisense-R6-Series-55-R6030-N-55-4-K-UHD-Roku-Smart-TV/484824.html?iref=rss-c142",
158
+ },
159
+ {
160
+ "product_description": "The Poly Studio P21 is a 21.5-inch LED personal meeting display designed specifically for remote work and video conferencing. With a native resolution of 1080p, it provides crystal-clear video quality, featuring a privacy shutter and stereo speakers. This display includes a 1080p webcam with manual pan, tilt, and zoom control, along with an ambient light sensor to adjust the vanity lighting as needed. It also supports 5W wireless charging for mobile devices, making it an all-in-one solution for home offices.",
161
+ "price": 30,
162
+ "url": "https://www.dealnews.com/products/Poly-Studio-P21-21-5-1080-p-LED-Personal-Meeting-Display/378335.html?iref=rss-c39",
163
+ },
164
+ {
165
+ "product_description": "The Lenovo IdeaPad Slim 5 laptop is powered by a 7th generation AMD Ryzen 5 8645HS 6-core CPU, offering efficient performance for multitasking and demanding applications. It features a 16-inch touch display with a resolution of 1920x1080, ensuring bright and vivid visuals. Accompanied by 16GB of RAM and a 512GB SSD, the laptop provides ample speed and storage for all your files. This model is designed to handle everyday tasks with ease while delivering an enjoyable user experience.",
166
+ "price": 446,
167
+ "url": "https://www.dealnews.com/products/Lenovo/Lenovo-Idea-Pad-Slim-5-7-th-Gen-Ryzen-5-16-Touch-Laptop/485068.html?iref=rss-c39",
168
+ },
169
+ {
170
+ "product_description": "The Dell G15 gaming laptop is equipped with a 6th-generation AMD Ryzen 5 7640HS 6-Core CPU, providing powerful performance for gaming and content creation. It features a 15.6-inch 1080p display with a 120Hz refresh rate, allowing for smooth and responsive gameplay. With 16GB of RAM and a substantial 1TB NVMe M.2 SSD, this laptop ensures speedy performance and plenty of storage for games and applications. Additionally, it includes the Nvidia GeForce RTX 3050 GPU for enhanced graphics and gaming experiences.",
171
+ "price": 650,
172
+ "url": "https://www.dealnews.com/products/Dell/Dell-G15-Ryzen-5-15-6-Gaming-Laptop-w-Nvidia-RTX-3050/485067.html?iref=rss-c39",
173
+ },
174
+ ]
175
+ }
176
+ return DealSelection(**results)
_hf_capstone_repo/agents/specialist_agent.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+
4
+ from agents.agent import Agent
5
+ from free_config import GROQ_MODEL, USE_MODAL_SPECIALIST
6
+
7
+ QUESTION = "What does this cost to the nearest dollar?"
8
+ PREFIX = "Price is $"
9
+
10
+
11
+ class SpecialistAgent(Agent):
12
+ """
13
+ Prices products using either:
14
+ 1. Fine-tuned Llama on Modal (best quality, uses GPU credits), or
15
+ 2. Groq fallback (free — same prompt format as the fine-tuned model)
16
+
17
+ Set USE_MODAL_SPECIALIST=true only if you have Modal credits to spare.
18
+ """
19
+
20
+ name = "Specialist Agent"
21
+ color = Agent.RED
22
+
23
+ def __init__(self):
24
+ self.pricer = None
25
+ self.groq_client = None
26
+ self.mode = "off"
27
+ self.model = GROQ_MODEL
28
+
29
+ if USE_MODAL_SPECIALIST:
30
+ self._try_modal()
31
+
32
+ if self.mode == "off" and os.getenv("GROQ_API_KEY"):
33
+ from groq import Groq
34
+
35
+ self.groq_client = Groq(api_key=os.environ["GROQ_API_KEY"])
36
+ self.mode = "groq"
37
+ self.log(f"Specialist Agent using Groq fallback ({self.model}) — no Modal GPU needed")
38
+
39
+ if self.mode == "off":
40
+ self.log("Specialist Agent disabled — ensemble will use Frontier + Neural Network only")
41
+
42
+ def _try_modal(self):
43
+ try:
44
+ import modal
45
+
46
+ self.log("Specialist Agent is initializing - connecting to Modal")
47
+ Pricer = modal.Cls.from_name("pricer-service", "Pricer")
48
+ self.pricer = Pricer()
49
+ self.mode = "modal"
50
+ self.log("Specialist Agent connected to Modal pricer-service")
51
+ except Exception as exc:
52
+ self.log(f"Modal unavailable ({exc}) — will try Groq fallback if configured")
53
+
54
+ @property
55
+ def available(self) -> bool:
56
+ return self.mode != "off"
57
+
58
+ @staticmethod
59
+ def _parse_price(text: str) -> float:
60
+ if PREFIX in text:
61
+ text = text.split(PREFIX, 1)[1]
62
+ text = text.replace("$", "").replace(",", "")
63
+ match = re.search(r"[-+]?\d*\.\d+|\d+", text)
64
+ return float(match.group()) if match else 0.0
65
+
66
+ def _price_with_groq(self, description: str) -> float:
67
+ prompt = f"{QUESTION}\n\n{description}\n\n{PREFIX}"
68
+ response = self.groq_client.chat.completions.create(
69
+ model=self.model,
70
+ messages=[{"role": "user", "content": prompt}],
71
+ temperature=0,
72
+ max_tokens=8,
73
+ )
74
+ reply = response.choices[0].message.content or ""
75
+ return self._parse_price(reply)
76
+
77
+ def price(self, description: str) -> float:
78
+ if self.mode == "modal":
79
+ self.log("Specialist Agent is calling remote fine-tuned model on Modal")
80
+ result = self.pricer.price.remote(description)
81
+ self.log(f"Specialist Agent completed - predicting ${result:.2f}")
82
+ return result
83
+
84
+ if self.mode == "groq":
85
+ self.log(f"Specialist Agent is calling Groq ({self.model})")
86
+ result = self._price_with_groq(description)
87
+ self.log(f"Specialist Agent completed - predicting ${result:.2f}")
88
+ return result
89
+
90
+ return 0.0
_hf_capstone_repo/app.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces entry point.
3
+
4
+ Upload the entire week8/ folder into the Space repo root (this file becomes app.py),
5
+ or copy week8/* into the Space and rename/move as needed.
6
+
7
+ Required Space secrets: GROQ_API_KEY, HF_TOKEN, USE_MODAL_SPECIALIST=false
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # When week8 is the Space root, agents/ and deal_agent_framework.py are siblings.
15
+ ROOT = Path(__file__).resolve().parent
16
+ if str(ROOT) not in sys.path:
17
+ sys.path.insert(0, str(ROOT))
18
+
19
+ os.environ.setdefault("GRADIO_SERVER_NAME", "0.0.0.0")
20
+ os.environ.setdefault("PRICER_PREPROCESSOR_MODEL", "groq/llama-3.1-8b-instant")
21
+
22
+ from price_is_right import App
23
+
24
+ if __name__ == "__main__":
25
+ App().run()
_hf_capstone_repo/deal_agent_framework.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import logging
4
+ import json
5
+ from typing import List
6
+ from dotenv import load_dotenv
7
+ import chromadb
8
+ from agents.planning_agent import PlanningAgent
9
+ from agents.deals import Opportunity
10
+ from sklearn.manifold import TSNE
11
+ import numpy as np
12
+
13
+ load_dotenv(override=True)
14
+
15
+ # Colors for logging
16
+ BG_BLUE = "\033[44m"
17
+ WHITE = "\033[37m"
18
+ RESET = "\033[0m"
19
+
20
+ # Colors for plot
21
+ CATEGORIES = [
22
+ "Appliances",
23
+ "Automotive",
24
+ "Cell_Phones_and_Accessories",
25
+ "Electronics",
26
+ "Musical_Instruments",
27
+ "Office_Products",
28
+ "Tools_and_Home_Improvement",
29
+ "Toys_and_Games",
30
+ ]
31
+ COLORS = ["red", "blue", "brown", "orange", "yellow", "green", "purple", "cyan"]
32
+
33
+
34
+ def init_logging():
35
+ root = logging.getLogger()
36
+ root.setLevel(logging.INFO)
37
+
38
+ handler = logging.StreamHandler(sys.stdout)
39
+ handler.setLevel(logging.INFO)
40
+ formatter = logging.Formatter(
41
+ "[%(asctime)s] [Agents] [%(levelname)s] %(message)s",
42
+ datefmt="%Y-%m-%d %H:%M:%S %z",
43
+ )
44
+ handler.setFormatter(formatter)
45
+ root.addHandler(handler)
46
+
47
+
48
+ class DealAgentFramework:
49
+ DB = "products_vectorstore"
50
+ MEMORY_FILENAME = "memory.json"
51
+
52
+ def __init__(self):
53
+ init_logging()
54
+ client = chromadb.PersistentClient(path=self.DB)
55
+ self.memory = self.read_memory()
56
+ self.collection = client.get_or_create_collection("products")
57
+ self.planner = None
58
+
59
+ def init_agents_as_needed(self):
60
+ if not self.planner:
61
+ self.log("Initializing Agent Framework")
62
+ self.planner = PlanningAgent(self.collection)
63
+ self.log("Agent Framework is ready")
64
+
65
+ def read_memory(self) -> List[Opportunity]:
66
+ if os.path.exists(self.MEMORY_FILENAME):
67
+ with open(self.MEMORY_FILENAME, "r") as file:
68
+ data = json.load(file)
69
+ opportunities = [Opportunity(**item) for item in data]
70
+ return opportunities
71
+ return []
72
+
73
+ def write_memory(self) -> None:
74
+ data = [opportunity.model_dump() for opportunity in self.memory]
75
+ with open(self.MEMORY_FILENAME, "w") as file:
76
+ json.dump(data, file, indent=2)
77
+
78
+ @classmethod
79
+ def reset_memory(cls) -> None:
80
+ data = []
81
+ if os.path.exists(cls.MEMORY_FILENAME):
82
+ with open(cls.MEMORY_FILENAME, "r") as file:
83
+ data = json.load(file)
84
+ truncated = data[:2]
85
+ with open(cls.MEMORY_FILENAME, "w") as file:
86
+ json.dump(truncated, file, indent=2)
87
+
88
+ def log(self, message: str):
89
+ text = BG_BLUE + WHITE + "[Agent Framework] " + message + RESET
90
+ logging.info(text)
91
+
92
+ def run(self) -> List[Opportunity]:
93
+ self.init_agents_as_needed()
94
+ logging.info("Kicking off Planning Agent")
95
+ result = self.planner.plan(memory=self.memory)
96
+ logging.info(f"Planning Agent has completed and returned: {result}")
97
+ if result:
98
+ self.memory.append(result)
99
+ self.write_memory()
100
+ return self.memory
101
+
102
+ @classmethod
103
+ def get_plot_data(cls, max_datapoints=2000):
104
+ client = chromadb.PersistentClient(path=cls.DB)
105
+ collection = client.get_or_create_collection("products")
106
+ result = collection.get(
107
+ include=["embeddings", "documents", "metadatas"], limit=max_datapoints
108
+ )
109
+ vectors = np.array(result["embeddings"])
110
+ documents = result["documents"]
111
+ categories = [metadata["category"] for metadata in result["metadatas"]]
112
+ colors = [COLORS[CATEGORIES.index(c)] for c in categories]
113
+ tsne = TSNE(n_components=3, random_state=42, n_jobs=-1)
114
+ reduced_vectors = tsne.fit_transform(vectors)
115
+ return documents, reduced_vectors, colors
116
+
117
+
118
+ if __name__ == "__main__":
119
+ DealAgentFramework().run()
_hf_capstone_repo/env_utils.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load .env from repo root or week8 folder."""
2
+
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ WEEK8_DIR = Path(__file__).resolve().parent
8
+ REPO_ROOT = WEEK8_DIR.parent
9
+
10
+
11
+ def load_project_env() -> None:
12
+ for path in (REPO_ROOT / ".env", WEEK8_DIR / ".env"):
13
+ if path.exists():
14
+ load_dotenv(path, override=True)
_hf_capstone_repo/free_config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Free-tier configuration for Week 8 — The Price is Right.
3
+
4
+ Set GROQ_API_KEY in .env for cloud LLM calls (local run + Hugging Face Spaces).
5
+ Set USE_MODAL_SPECIALIST=true only if you want the GPU fine-tuned model on Modal.
6
+ By default Modal is OFF — Specialist uses Groq instead (free).
7
+ """
8
+
9
+ import os
10
+
11
+ from env_utils import load_project_env
12
+
13
+ load_project_env()
14
+
15
+ GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
16
+ LITELLM_GROQ_MODEL = f"groq/{GROQ_MODEL}"
17
+
18
+ # Default false: Modal free credits are often only ~$1/month — not enough for regular GPU use.
19
+ USE_MODAL_SPECIALIST = os.getenv("USE_MODAL_SPECIALIST", "false").lower() in ("true", "1", "yes")
20
+
21
+ # Groq when an API key is present; otherwise local Ollama (local dev only).
22
+ PREPROCESSOR_MODEL = os.getenv(
23
+ "PRICER_PREPROCESSOR_MODEL",
24
+ LITELLM_GROQ_MODEL if os.getenv("GROQ_API_KEY") else "ollama/llama3.2",
25
+ )
26
+
27
+ MESSAGING_MODEL = os.getenv("MESSAGING_MODEL", LITELLM_GROQ_MODEL)
28
+
29
+ # Ensemble weights when Modal specialist is unavailable.
30
+ WEIGHTS_WITH_SPECIALIST = (0.8, 0.1, 0.1) # frontier, specialist, neural_network
31
+ WEIGHTS_WITHOUT_SPECIALIST = (0.85, 0.15) # frontier, neural_network
32
+
33
+ PUSHOVER_CONFIGURED = bool(
34
+ os.getenv("PUSHOVER_USER")
35
+ and os.getenv("PUSHOVER_TOKEN")
36
+ and not os.getenv("PUSHOVER_USER", "").startswith("your-")
37
+ )
_hf_capstone_repo/log_utils.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Foreground colors
2
+ RED = '\033[31m'
3
+ GREEN = '\033[32m'
4
+ YELLOW = '\033[33m'
5
+ BLUE = '\033[34m'
6
+ MAGENTA = '\033[35m'
7
+ CYAN = '\033[36m'
8
+ WHITE = '\033[37m'
9
+
10
+ # Background color
11
+ BG_BLACK = '\033[40m'
12
+ BG_BLUE = '\033[44m'
13
+
14
+ # Reset code to return to default color
15
+ RESET = '\033[0m'
16
+
17
+ mapper = {
18
+ BG_BLACK+RED: "#dd0000",
19
+ BG_BLACK+GREEN: "#00dd00",
20
+ BG_BLACK+YELLOW: "#dddd00",
21
+ BG_BLACK+BLUE: "#0000ee",
22
+ BG_BLACK+MAGENTA: "#aa00dd",
23
+ BG_BLACK+CYAN: "#00dddd",
24
+ BG_BLACK+WHITE: "#87CEEB",
25
+ BG_BLUE+WHITE: "#ff7800"
26
+ }
27
+
28
+
29
+ def reformat(message):
30
+ for key, value in mapper.items():
31
+ message = message.replace(key, f'<span style="color: {value}">')
32
+ message = message.replace(RESET, '</span>')
33
+ return message
34
+
35
+
_hf_capstone_repo/memory.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "deal": {
4
+ "product_description": "The Samsung Galaxy Watch Ultra is a premium 47mm LTE Titanium smartwatch designed for both style and functionality. It features a circular display made with durable materials suitable for outdoor activities, providing GPS tracking, health monitoring, and custom apps for various needs. The robust design integrates a range of smart features including notifications, music control, and heart rate tracking, making it an ideal companion for fitness enthusiasts and tech-savvy users alike.",
5
+ "price": 350.0,
6
+ "url": "https://www.dealnews.com/Samsung-Galaxy-Watch-Ultra-47-mm-LTE-Titanium-Smartwatch-up-to-350-off-w-Trade-in-free-shipping/21663266.html?iref=rss-c142"
7
+ },
8
+ "estimate": 773.8138460593241,
9
+ "discount": 423.8138460593241
10
+ },
11
+ {
12
+ "deal": {
13
+ "product_description": "The Refurbished Unlocked Apple iPhone 14 Pro Max offers an impressive 256GB storage and a huge display, perfect for both media consumption and productivity. Enjoy advanced camera technology for stunning photos. This model is designed to provide a seamless user experience with 5G capabilities for faster downloads and streaming. Refurbished to high standards, it comes in various colors and can support all the latest apps from the App Store, accommodating any Apple enthusiast's needs.",
14
+ "price": 705.0,
15
+ "url": "https://www.dealnews.com/products/Apple/Unlocked-Apple-iPhone-14-Pro-Max-256-GB-Smartphone/462808.html?iref=rss-c142"
16
+ },
17
+ "estimate": 930.8824204895075,
18
+ "discount": 225.88242048950747
19
+ },
20
+ {
21
+ "deal": {
22
+ "product_description": "This Vizio VQM55C-10 55 inch 4K QLED UHD Smart TV features 4K resolution with HDR support, Apple AirPlay 2, Google Cast, and a voice remote.",
23
+ "price": 248.0,
24
+ "url": "https://www.dealnews.com/Vizio-VQM55-C-10-55-4-K-QLED-UHD-Smart-TV-for-248-free-shipping/21998056.html?iref=rss-c142"
25
+ },
26
+ "estimate": 592.7400194091798,
27
+ "discount": 344.7400194091798
28
+ }
29
+ ]
_hf_capstone_repo/price_is_right.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import queue
4
+ import threading
5
+ import time
6
+
7
+ import gradio as gr
8
+ import plotly.graph_objects as go
9
+ from dotenv import load_dotenv
10
+
11
+ from deal_agent_framework import DealAgentFramework
12
+ from log_utils import reformat
13
+
14
+ load_dotenv(override=True)
15
+
16
+
17
+ class QueueHandler(logging.Handler):
18
+ def __init__(self, log_queue):
19
+ super().__init__()
20
+ self.log_queue = log_queue
21
+
22
+ def emit(self, record):
23
+ self.log_queue.put(self.format(record))
24
+
25
+
26
+ def html_for(log_data):
27
+ output = "<br>".join(log_data[-18:])
28
+ return f"""
29
+ <div id="scrollContent" style="height: 400px; overflow-y: auto; border: 1px solid #ccc; background-color: #222229; padding: 10px;">
30
+ {output}
31
+ </div>
32
+ """
33
+
34
+
35
+ def setup_logging(log_queue):
36
+ handler = QueueHandler(log_queue)
37
+ formatter = logging.Formatter(
38
+ "[%(asctime)s] %(message)s",
39
+ datefmt="%Y-%m-%d %H:%M:%S %z",
40
+ )
41
+ handler.setFormatter(formatter)
42
+ logger = logging.getLogger()
43
+ logger.addHandler(handler)
44
+ logger.setLevel(logging.INFO)
45
+
46
+
47
+ class App:
48
+ def __init__(self):
49
+ self.agent_framework = None
50
+
51
+ def get_agent_framework(self):
52
+ if not self.agent_framework:
53
+ self.agent_framework = DealAgentFramework()
54
+ return self.agent_framework
55
+
56
+ def run(self):
57
+ with gr.Blocks(title="The Price is Right", fill_width=True) as ui:
58
+ log_data = gr.State([])
59
+
60
+ def table_for(opps):
61
+ return [
62
+ [
63
+ opp.deal.product_description,
64
+ f"${opp.deal.price:.2f}",
65
+ f"${opp.estimate:.2f}",
66
+ f"${opp.discount:.2f}",
67
+ opp.deal.url,
68
+ ]
69
+ for opp in opps
70
+ ]
71
+
72
+ def update_output(log_data, log_queue, result_queue):
73
+ initial_result = table_for(self.get_agent_framework().memory)
74
+ final_result = None
75
+ while True:
76
+ try:
77
+ message = log_queue.get_nowait()
78
+ log_data.append(reformat(message))
79
+ yield log_data, html_for(log_data), final_result or initial_result
80
+ except queue.Empty:
81
+ try:
82
+ final_result = result_queue.get_nowait()
83
+ yield log_data, html_for(log_data), final_result or initial_result
84
+ except queue.Empty:
85
+ if final_result is not None:
86
+ break
87
+ time.sleep(0.1)
88
+
89
+ def get_initial_plot():
90
+ fig = go.Figure()
91
+ fig.update_layout(
92
+ title="Loading vector DB...",
93
+ height=400,
94
+ )
95
+ return fig
96
+
97
+ def get_plot():
98
+ documents, vectors, colors = DealAgentFramework.get_plot_data(max_datapoints=800)
99
+ # Create the 3D scatter plot
100
+ fig = go.Figure(
101
+ data=[
102
+ go.Scatter3d(
103
+ x=vectors[:, 0],
104
+ y=vectors[:, 1],
105
+ z=vectors[:, 2],
106
+ mode="markers",
107
+ marker=dict(size=2, color=colors, opacity=0.7),
108
+ )
109
+ ]
110
+ )
111
+
112
+ fig.update_layout(
113
+ scene=dict(
114
+ xaxis_title="x",
115
+ yaxis_title="y",
116
+ zaxis_title="z",
117
+ aspectmode="manual",
118
+ aspectratio=dict(x=2.2, y=2.2, z=1), # Make x-axis twice as long
119
+ camera=dict(
120
+ eye=dict(x=1.6, y=1.6, z=0.8) # Adjust camera position
121
+ ),
122
+ ),
123
+ height=400,
124
+ margin=dict(r=5, b=1, l=5, t=2),
125
+ )
126
+
127
+ return fig
128
+
129
+ def do_run():
130
+ new_opportunities = self.get_agent_framework().run()
131
+ table = table_for(new_opportunities)
132
+ return table
133
+
134
+ def run_with_logging(initial_log_data):
135
+ log_queue = queue.Queue()
136
+ result_queue = queue.Queue()
137
+ setup_logging(log_queue)
138
+
139
+ def worker():
140
+ result = do_run()
141
+ result_queue.put(result)
142
+
143
+ thread = threading.Thread(target=worker)
144
+ thread.start()
145
+
146
+ for log_data, output, final_result in update_output(
147
+ initial_log_data, log_queue, result_queue
148
+ ):
149
+ yield log_data, output, final_result
150
+
151
+ def do_select(selected_index: gr.SelectData):
152
+ opportunities = self.get_agent_framework().memory
153
+ row = selected_index.index[0]
154
+ opportunity = opportunities[row]
155
+ self.get_agent_framework().planner.messenger.alert(opportunity)
156
+
157
+ with gr.Row():
158
+ gr.Markdown(
159
+ '<div style="text-align: center;font-size:24px"><strong>The Price is Right</strong> - Autonomous Agent Framework that hunts for deals</div>'
160
+ )
161
+ with gr.Row():
162
+ gr.Markdown(
163
+ '<div style="text-align: center;font-size:14px">Multi-agent deal hunter: Groq LLM + RAG + optional Modal fine-tuned pricer. Finds online deals and estimates true product value.</div>'
164
+ )
165
+ with gr.Row():
166
+ opportunities_dataframe = gr.Dataframe(
167
+ headers=["Deals found so far", "Price", "Estimate", "Discount", "URL"],
168
+ wrap=True,
169
+ column_widths=[6, 1, 1, 1, 3],
170
+ row_count=10,
171
+ col_count=5,
172
+ max_height=400,
173
+ )
174
+ with gr.Row():
175
+ with gr.Column(scale=1):
176
+ logs = gr.HTML()
177
+ with gr.Column(scale=1):
178
+ plot = gr.Plot(value=get_plot(), show_label=False)
179
+
180
+ ui.load(
181
+ run_with_logging,
182
+ inputs=[log_data],
183
+ outputs=[log_data, logs, opportunities_dataframe],
184
+ )
185
+
186
+ timer = gr.Timer(value=300, active=True)
187
+ timer.tick(
188
+ run_with_logging,
189
+ inputs=[log_data],
190
+ outputs=[log_data, logs, opportunities_dataframe],
191
+ )
192
+
193
+ opportunities_dataframe.select(do_select)
194
+
195
+ share = os.getenv("GRADIO_SHARE", "false").lower() in ("true", "1", "yes")
196
+ server_name = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
197
+ server_port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
198
+ inbrowser = server_name in ("127.0.0.1", "localhost") and not share
199
+ ui.launch(
200
+ share=share,
201
+ server_name=server_name,
202
+ server_port=server_port,
203
+ inbrowser=inbrowser,
204
+ )
205
+
206
+
207
+ if __name__ == "__main__":
208
+ App().run()
_hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/data_level0.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c03e1e12c5de42db678c5308d0f09ad904f04c7dd8ee5be04be405dfc8d46c42
3
+ size 33520000
_hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/header.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a8615225934162b4e8f0cae7ce25caa91aa27f6f3c15d95e448ce5434c8c406
3
+ size 100
_hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/index_metadata.pickle ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4cac2085f7a9d810683f02dc31dc76daaf7b5b6658e6e322863829d174c7f84
3
+ size 737988
_hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/length.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:881cb357be5036b37fd074699068c05e500cf3f2e934057c3214059a8655f886
3
+ size 80000
_hf_capstone_repo/products_vectorstore/c8498423-ab24-462f-a1fc-d9ef5751968d/link_lists.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5736f0e6ca098dc3cf00ae4ec55862a15c1a8dd03a4fb751506dc5b6419cbee
3
+ size 171800
_hf_capstone_repo/products_vectorstore/chroma.sqlite3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b3cedf0c5c60f537a29524c6d05669bd74d343dd18bb95f5e247567d94b2f2a
3
+ size 59838464
_hf_capstone_repo/requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.47.2,<6.0
2
+ chromadb>=1.1.0
3
+ sentence-transformers>=5.1.1
4
+ groq>=0.33.0
5
+ litellm>=1.77.5
6
+ python-dotenv>=1.1.1
7
+ pydantic>=2.0
8
+ beautifulsoup4>=4.14.2
9
+ feedparser>=6.0.12
10
+ requests>=2.32.5
11
+ numpy>=2.3.3
12
+ scikit-learn>=1.7.2
13
+ plotly>=6.3.0
14
+ pandas>=2.3.3
15
+ torch>=2.8.0
16
+ modal>=1.1.4
17
+ tqdm>=4.67.1
_hf_deploy/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ products_vectorstore/** filter=lfs diff=lfs merge=lfs -text
_hf_deploy/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Capstone - The Price Is Right
3
+ emoji: 💰
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "5.47.2"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Capstone — The Price Is Right
14
+
15
+ Multi-agent deal hunting AI for the LLM Engineering capstone project.
16
+
17
+ - Scans DealNews RSS feeds for deals
18
+ - Estimates true product prices with RAG + Groq + neural network
19
+ - Surfaces bargains in a live Gradio dashboard
20
+
21
+ **Secrets required:** `GROQ_API_KEY`, `HF_TOKEN`, `USE_MODAL_SPECIALIST=false`, `GRADIO_SERVER_NAME=0.0.0.0`
_hf_deploy/agents/agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ class Agent:
4
+ """
5
+ An abstract superclass for Agents
6
+ Used to log messages in a way that can identify each Agent
7
+ """
8
+
9
+ # Foreground colors
10
+ RED = '\033[31m'
11
+ GREEN = '\033[32m'
12
+ YELLOW = '\033[33m'
13
+ BLUE = '\033[34m'
14
+ MAGENTA = '\033[35m'
15
+ CYAN = '\033[36m'
16
+ WHITE = '\033[37m'
17
+
18
+ # Background color
19
+ BG_BLACK = '\033[40m'
20
+
21
+ # Reset code to return to default color
22
+ RESET = '\033[0m'
23
+
24
+ name: str = ""
25
+ color: str = '\033[37m'
26
+
27
+ def log(self, message):
28
+ """
29
+ Log this as an info message, identifying the agent
30
+ """
31
+ color_code = self.BG_BLACK + self.color
32
+ message = f"[{self.name}] {message}"
33
+ logging.info(color_code + message + self.RESET)
_hf_deploy/agents/autonomous_planning_agent.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List, Dict
2
+ from agents.agent import Agent
3
+ from agents.deals import Deal, Opportunity
4
+ from agents.scanner_agent import ScannerAgent
5
+ from agents.ensemble_agent import EnsembleAgent
6
+ from agents.messaging_agent import MessagingAgent
7
+ from openai import OpenAI
8
+ import json
9
+
10
+
11
+ class AutonomousPlanningAgent(Agent):
12
+ name = "Autonomous Planning Agent"
13
+ color = Agent.GREEN
14
+ MODEL = "gpt-5.1"
15
+
16
+ def __init__(self, collection):
17
+ """
18
+ Create instances of the 3 Agents that this planner coordinates across
19
+ """
20
+ self.log("Autonomous Planning Agent is initializing")
21
+ self.scanner = ScannerAgent()
22
+ self.ensemble = EnsembleAgent(collection)
23
+ self.messenger = MessagingAgent()
24
+ self.openai = OpenAI()
25
+ self.memory = None
26
+ self.opportunity = None
27
+ self.log("Autonomous Planning Agent is ready")
28
+
29
+ def scan_the_internet_for_bargains(self) -> str:
30
+ """
31
+ Run the tool to scan
32
+ """
33
+ self.log("Autonomous Planning agent is calling scanner")
34
+ results = self.scanner.scan(memory=self.memory)
35
+ return results.model_dump_json() if results else "No deals found"
36
+
37
+ def estimate_true_value(self, description: str) -> str:
38
+ """
39
+ Run the tool to estimate true value
40
+ """
41
+ self.log("Autonomous Planning agent is estimating value via Ensemble Agent")
42
+ estimate = self.ensemble.price(description)
43
+ return f"The estimated true value of {description} is {estimate}"
44
+
45
+ def notify_user_of_deal(
46
+ self, description: str, deal_price: float, estimated_true_value: float, url: str
47
+ ) -> Dict:
48
+ """
49
+ Run the tool to notify the user
50
+ """
51
+ if self.opportunity:
52
+ self.log("Autonomous Planning agent is trying to notify the user a 2nd time; ignoring")
53
+ else:
54
+ self.log("Autonomous Planning agent is notifying user")
55
+ self.messenger.notify(description, deal_price, estimated_true_value, url)
56
+ deal = Deal(product_description=description, price=deal_price, url=url)
57
+ discount = estimated_true_value - deal_price
58
+ self.opportunity = Opportunity(
59
+ deal=deal, estimate=estimated_true_value, discount=discount
60
+ )
61
+ return "Notification sent ok"
62
+
63
+ scan_function = {
64
+ "name": "scan_the_internet_for_bargains",
65
+ "description": "Returns top bargains scraped from the internet along with the price each item is being offered for",
66
+ "parameters": {
67
+ "type": "object",
68
+ "properties": {},
69
+ "required": [],
70
+ "additionalProperties": False,
71
+ },
72
+ }
73
+
74
+ estimate_function = {
75
+ "name": "estimate_true_value",
76
+ "description": "Given the description of an item, estimate how much it is actually worth",
77
+ "parameters": {
78
+ "type": "object",
79
+ "properties": {
80
+ "description": {
81
+ "type": "string",
82
+ "description": "The description of the item to be estimated",
83
+ },
84
+ },
85
+ "required": ["description"],
86
+ "additionalProperties": False,
87
+ },
88
+ }
89
+
90
+ notify_function = {
91
+ "name": "notify_user_of_deal",
92
+ "description": "Send the user a push notification about the single most compelling deal; only call this one time",
93
+ "parameters": {
94
+ "type": "object",
95
+ "properties": {
96
+ "description": {
97
+ "type": "string",
98
+ "description": "The description of the item itself scraped from the internet",
99
+ },
100
+ "deal_price": {
101
+ "type": "number",
102
+ "description": "The price offered by this deal scraped from the internet",
103
+ },
104
+ "estimated_true_value": {
105
+ "type": "number",
106
+ "description": "The estimated actual value that this is worth",
107
+ },
108
+ "url": {
109
+ "type": "string",
110
+ "description": "The URL of this deal as scraped from the internet",
111
+ },
112
+ },
113
+ "required": ["description", "deal_price", "estimated_true_value", "url"],
114
+ "additionalProperties": False,
115
+ },
116
+ }
117
+
118
+ def get_tools(self):
119
+ """
120
+ Return the json for the tools to be used
121
+ """
122
+ return [
123
+ {"type": "function", "function": self.scan_function},
124
+ {"type": "function", "function": self.estimate_function},
125
+ {"type": "function", "function": self.notify_function},
126
+ ]
127
+
128
+ def handle_tool_call(self, message):
129
+ """
130
+ Actually call the tools associated with this message
131
+ """
132
+ mapping = {
133
+ "scan_the_internet_for_bargains": self.scan_the_internet_for_bargains,
134
+ "estimate_true_value": self.estimate_true_value,
135
+ "notify_user_of_deal": self.notify_user_of_deal,
136
+ }
137
+ results = []
138
+ for tool_call in message.tool_calls:
139
+ tool_name = tool_call.function.name
140
+ arguments = json.loads(tool_call.function.arguments)
141
+ tool = mapping.get(tool_name)
142
+ result = tool(**arguments) if tool else ""
143
+ results.append({"role": "tool", "content": result, "tool_call_id": tool_call.id})
144
+ return results
145
+
146
+ system_message = "You find great deals on bargain products using your tools, and notify the user of the best bargain."
147
+ user_message = """
148
+ First, use your tool to scan the internet for bargain deals. Then for each deal, use your tool to estimate its true value.
149
+ Then pick the single most compelling deal where the price is much lower than the estimated true value, and use your tool to notify the user.
150
+ Then just reply OK to indicate success.
151
+ """
152
+ messages = [
153
+ {"role": "system", "content": system_message},
154
+ {"role": "user", "content": user_message},
155
+ ]
156
+
157
+ def plan(self, memory: List[str] = []) -> Optional[Opportunity]:
158
+ """
159
+ Run the full workflow, providing the LLM with tools to surface scraped deals to the user
160
+ :param memory: a list of URLs that have been surfaced in the past
161
+ :return: an Opportunity if one was surfaced, otherwise None
162
+ """
163
+ self.log("Autonomous Planning Agent is kicking off a run")
164
+ self.memory = memory
165
+ self.opportunity = None
166
+ messages = self.messages[:]
167
+ done = False
168
+ while not done:
169
+ response = self.openai.chat.completions.create(
170
+ model=self.MODEL, messages=messages, tools=self.get_tools()
171
+ )
172
+ if response.choices[0].finish_reason == "tool_calls":
173
+ message = response.choices[0].message
174
+ results = self.handle_tool_call(message)
175
+ messages.append(message)
176
+ messages.extend(results)
177
+ else:
178
+ done = True
179
+ reply = response.choices[0].message.content
180
+ self.log(f"Autonomous Planning Agent completed with: {reply}")
181
+ return self.opportunity
_hf_deploy/agents/deals.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Dict, Self
3
+ from bs4 import BeautifulSoup
4
+ import re
5
+ import feedparser
6
+ from tqdm import tqdm
7
+ import requests
8
+ import time
9
+
10
+ feeds = [
11
+ "https://www.dealnews.com/c142/Electronics/?rss=1",
12
+ "https://www.dealnews.com/c39/Computers/?rss=1",
13
+ "https://www.dealnews.com/f1912/Smart-Home/?rss=1",
14
+ ]
15
+
16
+ # You could also add: "https://www.dealnews.com/c238/Automotive/?rss=1"
17
+ # "https://www.dealnews.com/c196/Home-Garden/?rss=1"
18
+
19
+
20
+ def extract(html_snippet: str) -> str:
21
+ """
22
+ Use Beautiful Soup to clean up this HTML snippet and extract useful text
23
+ """
24
+ soup = BeautifulSoup(html_snippet, "html.parser")
25
+ snippet_div = soup.find("div", class_="snippet summary")
26
+
27
+ if snippet_div:
28
+ description = snippet_div.get_text(strip=True)
29
+ description = BeautifulSoup(description, "html.parser").get_text()
30
+ description = re.sub("<[^<]+?>", "", description)
31
+ result = description.strip()
32
+ else:
33
+ result = html_snippet
34
+ return result.replace("\n", " ")
35
+
36
+
37
+ class ScrapedDeal:
38
+ """
39
+ A class to represent a Deal retrieved from an RSS feed
40
+ """
41
+
42
+ category: str
43
+ title: str
44
+ summary: str
45
+ url: str
46
+ details: str
47
+ features: str
48
+
49
+ def __init__(self, entry: Dict[str, str]):
50
+ """
51
+ Populate this instance based on the provided dict
52
+ """
53
+ self.title = entry["title"]
54
+ self.summary = extract(entry["summary"])
55
+ self.url = entry["links"][0]["href"]
56
+ stuff = requests.get(self.url).content
57
+ soup = BeautifulSoup(stuff, "html.parser")
58
+ content = soup.find("div", class_="content-section").get_text()
59
+ content = content.replace("\nmore", "").replace("\n", " ")
60
+ if "Features" in content:
61
+ self.details, self.features = content.split("Features", 1)
62
+ else:
63
+ self.details = content
64
+ self.features = ""
65
+ self.truncate()
66
+
67
+ def truncate(self):
68
+ """
69
+ Limit the fields to a sensible length to avoid sending too much info to the model
70
+ """
71
+ self.title = self.title[:100]
72
+ self.details = self.details[:500]
73
+ self.features = self.features[:500]
74
+
75
+ def __repr__(self):
76
+ """
77
+ Return a string to describe this deal
78
+ """
79
+ return f"<{self.title}>"
80
+
81
+ def describe(self):
82
+ """
83
+ Return a longer string to describe this deal for use in calling a model
84
+ """
85
+ return f"Title: {self.title}\nDetails: {self.details.strip()}\nFeatures: {self.features.strip()}\nURL: {self.url}"
86
+
87
+ @classmethod
88
+ def fetch(cls, show_progress: bool = False) -> List[Self]:
89
+ """
90
+ Retrieve all deals from the selected RSS feeds
91
+ """
92
+ deals = []
93
+ feed_iter = tqdm(feeds) if show_progress else feeds
94
+ for feed_url in feed_iter:
95
+ feed = feedparser.parse(feed_url)
96
+ for entry in feed.entries[:10]:
97
+ deals.append(cls(entry))
98
+ time.sleep(0.05)
99
+ return deals
100
+
101
+
102
+ class Deal(BaseModel):
103
+ """
104
+ A class to Represent a Deal with a summary description
105
+ """
106
+
107
+ product_description: str = Field(
108
+ description="Your clearly expressed summary of the product in 3-4 sentences. Details of the item are much more important than why it's a good deal. Avoid mentioning discounts and coupons; focus on the item itself. There should be a short paragraph of text for each item you choose."
109
+ )
110
+ price: float = Field(
111
+ description="The actual price of this product, as advertised in the deal. Be sure to give the actual price; for example, if a deal is described as $100 off the usual $300 price, you should respond with $200"
112
+ )
113
+ url: str = Field(description="The URL of the deal, as provided in the input")
114
+
115
+
116
+ class DealSelection(BaseModel):
117
+ """
118
+ A class to Represent a list of Deals
119
+ """
120
+
121
+ deals: List[Deal] = Field(
122
+ description="Your selection of the 5 deals that have the most detailed, high quality description and the most clear price. You should be confident that the price reflects the deal, that it is a good deal, with a clear description"
123
+ )
124
+
125
+
126
+ class Opportunity(BaseModel):
127
+ """
128
+ A class to represent a possible opportunity: a Deal where we estimate
129
+ it should cost more than it's being offered
130
+ """
131
+
132
+ deal: Deal
133
+ estimate: float
134
+ discount: float
_hf_deploy/agents/deep_neural_network.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from tqdm.notebook import tqdm
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.optim as optim
6
+ from torch.utils.data import DataLoader, TensorDataset
7
+ from torch.optim.lr_scheduler import CosineAnnealingLR
8
+ from sklearn.feature_extraction.text import HashingVectorizer
9
+ import logging
10
+
11
+
12
+ class ResidualBlock(nn.Module):
13
+ def __init__(self, hidden_size, dropout_prob):
14
+ super(ResidualBlock, self).__init__()
15
+ self.block = nn.Sequential(
16
+ nn.Linear(hidden_size, hidden_size),
17
+ nn.LayerNorm(hidden_size),
18
+ nn.ReLU(),
19
+ nn.Dropout(dropout_prob),
20
+ nn.Linear(hidden_size, hidden_size),
21
+ nn.LayerNorm(hidden_size),
22
+ )
23
+ self.relu = nn.ReLU()
24
+
25
+ def forward(self, x):
26
+ residual = x
27
+ out = self.block(x)
28
+ out += residual # Skip connection
29
+ return self.relu(out)
30
+
31
+
32
+ class DeepNeuralNetwork(nn.Module):
33
+ def __init__(self, input_size, num_layers=10, hidden_size=4096, dropout_prob=0.2):
34
+ super(DeepNeuralNetwork, self).__init__()
35
+
36
+ # First layer
37
+ self.input_layer = nn.Sequential(
38
+ nn.Linear(input_size, hidden_size),
39
+ nn.LayerNorm(hidden_size),
40
+ nn.ReLU(),
41
+ nn.Dropout(dropout_prob),
42
+ )
43
+
44
+ # Residual blocks
45
+ self.residual_blocks = nn.ModuleList()
46
+ for i in range(num_layers - 2):
47
+ self.residual_blocks.append(ResidualBlock(hidden_size, dropout_prob))
48
+
49
+ # Output layer
50
+ self.output_layer = nn.Linear(hidden_size, 1)
51
+
52
+ def forward(self, x):
53
+ x = self.input_layer(x)
54
+
55
+ for block in self.residual_blocks:
56
+ x = block(x)
57
+
58
+ return self.output_layer(x)
59
+
60
+
61
+ Y_STD = 1.0328539609909058
62
+ Y_MEAN = 4.434937953948975
63
+
64
+
65
+ class DeepNeuralNetworkInference:
66
+ def __init__(self):
67
+ self.vectorizer = None
68
+ self.model = None
69
+ self.device = None
70
+
71
+ np.random.seed(42)
72
+ torch.manual_seed(42)
73
+ torch.cuda.manual_seed(42)
74
+
75
+ def setup(self):
76
+ self.vectorizer = HashingVectorizer(n_features=5000, stop_words="english", binary=True)
77
+ self.model = DeepNeuralNetwork(5000)
78
+ if torch.cuda.is_available():
79
+ self.device = torch.device("cuda")
80
+ elif torch.backends.mps.is_available():
81
+ self.device = torch.device("mps")
82
+ else:
83
+ self.device = torch.device("cpu")
84
+
85
+ logging.info(f"Neural Network is using {self.device}")
86
+
87
+ self.model.to(self.device)
88
+
89
+ def load(self, path):
90
+ self.model.load_state_dict(torch.load(path, map_location=self.device))
91
+ self.model.to(self.device)
92
+
93
+ def inference(self, text):
94
+ self.model.eval()
95
+ with torch.no_grad():
96
+ vector = self.vectorizer.transform([text])
97
+ vector = torch.FloatTensor(vector.toarray()).to(self.device)
98
+ pred = self.model(vector)[0]
99
+ result = torch.exp(pred * Y_STD + Y_MEAN) - 1
100
+ result = result.item()
101
+ return max(0, result)
_hf_deploy/agents/ensemble_agent.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.agent import Agent
2
+ from agents.specialist_agent import SpecialistAgent
3
+ from agents.frontier_agent import FrontierAgent
4
+ from agents.neural_network_agent import NeuralNetworkAgent
5
+ from agents.preprocessor import Preprocessor
6
+ from free_config import WEIGHTS_WITH_SPECIALIST, WEIGHTS_WITHOUT_SPECIALIST
7
+
8
+
9
+ class EnsembleAgent(Agent):
10
+ name = "Ensemble Agent"
11
+ color = Agent.YELLOW
12
+
13
+ def __init__(self, collection):
14
+ """
15
+ Create an instance of Ensemble, by creating each of the models
16
+ And loading the weights of the Ensemble
17
+ """
18
+ self.log("Initializing Ensemble Agent")
19
+ self.specialist = SpecialistAgent()
20
+ self.frontier = FrontierAgent(collection)
21
+ self.neural_network = NeuralNetworkAgent()
22
+ self.preprocessor = Preprocessor()
23
+ self.use_specialist = self.specialist.available
24
+ if self.use_specialist:
25
+ mode = getattr(self.specialist, "mode", "on")
26
+ self.log(f"Ensemble Agent is ready (Frontier + Specialist [{mode}] + Neural Network)")
27
+ else:
28
+ self.log("Ensemble Agent is ready (Frontier + Neural Network only)")
29
+
30
+ def price(self, description: str) -> float:
31
+ """
32
+ Run this ensemble model
33
+ Ask each of the models to price the product
34
+ Then return the weighted average price
35
+ """
36
+ self.log("Running Ensemble Agent - preprocessing text")
37
+ rewrite = self.preprocessor.preprocess(description)
38
+ self.log(f"Pre-processed text using {self.preprocessor.model_name}")
39
+ frontier = self.frontier.price(rewrite)
40
+ neural_network = self.neural_network.price(rewrite)
41
+
42
+ if self.use_specialist:
43
+ specialist = self.specialist.price(rewrite)
44
+ wf, ws, wn = WEIGHTS_WITH_SPECIALIST
45
+ combined = frontier * wf + specialist * ws + neural_network * wn
46
+ else:
47
+ wf, wn = WEIGHTS_WITHOUT_SPECIALIST
48
+ combined = frontier * wf + neural_network * wn
49
+
50
+ self.log(f"Ensemble Agent complete - returning ${combined:.2f}")
51
+ return combined
_hf_deploy/agents/evaluator.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from sklearn.metrics import mean_squared_error, r2_score
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from itertools import accumulate
7
+ import math
8
+ from tqdm.notebook import tqdm
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ GREEN = "\033[92m"
12
+ YELLOW = "\033[93m"
13
+ RED = "\033[91m"
14
+ RESET = "\033[0m"
15
+ COLOR_MAP = {"red": RED, "orange": YELLOW, "green": GREEN}
16
+
17
+ WORKERS = 5
18
+ DEFAULT_SIZE = 200
19
+
20
+
21
+ class Tester:
22
+ def __init__(self, predictor, data, title=None, size=DEFAULT_SIZE, workers=WORKERS):
23
+ self.predictor = predictor
24
+ self.data = data
25
+ self.title = title or self.make_title(predictor)
26
+ self.size = size
27
+ self.titles = []
28
+ self.guesses = []
29
+ self.truths = []
30
+ self.errors = []
31
+ self.colors = []
32
+ self.workers = workers
33
+
34
+ @staticmethod
35
+ def make_title(predictor) -> str:
36
+ return predictor.__name__.replace("__", ".").replace("_", " ").title().replace("Gpt", "GPT")
37
+
38
+ @staticmethod
39
+ def post_process(value):
40
+ if isinstance(value, str):
41
+ value = value.replace("$", "").replace(",", "")
42
+ match = re.search(r"[-+]?\d*\.\d+|\d+", value)
43
+ return float(match.group()) if match else 0
44
+ else:
45
+ return value
46
+
47
+ def color_for(self, error, truth):
48
+ if error < 40 or error / truth < 0.2:
49
+ return "green"
50
+ elif error < 80 or error / truth < 0.4:
51
+ return "orange"
52
+ else:
53
+ return "red"
54
+
55
+ def run_datapoint(self, i):
56
+ datapoint = self.data[i]
57
+ value = self.predictor(datapoint)
58
+ guess = self.post_process(value)
59
+ truth = datapoint.price
60
+ error = abs(guess - truth)
61
+ color = self.color_for(error, truth)
62
+ title = datapoint.title if len(datapoint.title) <= 40 else datapoint.title[:40] + "..."
63
+ return title, guess, truth, error, color
64
+
65
+ def chart(self, title):
66
+ df = pd.DataFrame(
67
+ {
68
+ "truth": self.truths,
69
+ "guess": self.guesses,
70
+ "title": self.titles,
71
+ "error": self.errors,
72
+ "color": self.colors,
73
+ }
74
+ )
75
+
76
+ # Pre-format hover text
77
+ df["hover"] = [
78
+ f"{t}\nGuess=${g:,.2f} Actual=${y:,.2f}"
79
+ for t, g, y in zip(df["title"], df["guess"], df["truth"])
80
+ ]
81
+
82
+ max_val = float(max(df["truth"].max(), df["guess"].max()))
83
+
84
+ fig = px.scatter(
85
+ df,
86
+ x="truth",
87
+ y="guess",
88
+ color="color",
89
+ color_discrete_map={"green": "green", "orange": "orange", "red": "red"},
90
+ title=title,
91
+ labels={"truth": "Actual Price", "guess": "Predicted Price"},
92
+ width=1000,
93
+ height=800,
94
+ )
95
+
96
+ # Assign customdata per trace (one color/category = one trace)
97
+ for tr in fig.data:
98
+ mask = df["color"] == tr.name
99
+ tr.customdata = df.loc[mask, ["hover"]].to_numpy()
100
+ tr.hovertemplate = "%{customdata[0]}<extra></extra>"
101
+ tr.marker.update(size=6)
102
+
103
+ # Reference line y=x
104
+ fig.add_trace(
105
+ go.Scatter(
106
+ x=[0, max_val],
107
+ y=[0, max_val],
108
+ mode="lines",
109
+ line=dict(width=2, dash="dash", color="deepskyblue"),
110
+ name="y = x",
111
+ hoverinfo="skip",
112
+ showlegend=False,
113
+ )
114
+ )
115
+
116
+ fig.update_xaxes(range=[0, max_val])
117
+ fig.update_yaxes(range=[0, max_val])
118
+ fig.update_layout(showlegend=False)
119
+ fig.show()
120
+
121
+ def error_trend_chart(self):
122
+ n = len(self.errors)
123
+
124
+ # Running mean and std (pure Python)
125
+ running_sums = list(accumulate(self.errors))
126
+ x = list(range(1, n + 1))
127
+ running_means = [s / i for s, i in zip(running_sums, x)]
128
+
129
+ running_squares = list(accumulate(e * e for e in self.errors))
130
+ running_stds = [
131
+ math.sqrt((sq_sum / i) - (mean**2)) if i > 1 else 0
132
+ for i, sq_sum, mean in zip(x, running_squares, running_means)
133
+ ]
134
+
135
+ # 95% confidence interval for mean
136
+ ci = [1.96 * (sd / math.sqrt(i)) if i > 1 else 0 for i, sd in zip(x, running_stds)]
137
+ upper = [m + c for m, c in zip(running_means, ci)]
138
+ lower = [m - c for m, c in zip(running_means, ci)]
139
+
140
+ # Plot
141
+ fig = go.Figure()
142
+
143
+ # Shaded confidence interval band
144
+ fig.add_trace(
145
+ go.Scatter(
146
+ x=x + x[::-1],
147
+ y=upper + lower[::-1],
148
+ fill="toself",
149
+ fillcolor="rgba(128,128,128,0.2)",
150
+ line=dict(color="rgba(255,255,255,0)"),
151
+ hoverinfo="skip",
152
+ showlegend=False,
153
+ name="95% CI",
154
+ )
155
+ )
156
+
157
+ # Main line with hover text showing CI
158
+ fig.add_trace(
159
+ go.Scatter(
160
+ x=x,
161
+ y=running_means,
162
+ mode="lines",
163
+ line=dict(width=3, color="firebrick"),
164
+ name="Cumulative Avg Error",
165
+ customdata=list(
166
+ zip(
167
+ ci,
168
+ )
169
+ ),
170
+ hovertemplate=(
171
+ "n=%{x}<br>"
172
+ "Avg Error=$%{y:,.2f}<br>"
173
+ "±95% CI=$%{customdata[0]:,.2f}<extra></extra>"
174
+ ),
175
+ )
176
+ )
177
+
178
+ # Title with final stats
179
+ final_mean = running_means[-1]
180
+ final_ci = ci[-1]
181
+ title = f"{self.title} Error: ${final_mean:,.2f} ± ${final_ci:,.2f}"
182
+
183
+ fig.update_layout(
184
+ title=title,
185
+ xaxis_title="Number of Datapoints",
186
+ yaxis_title="Average Absolute Error ($)",
187
+ width=1000,
188
+ height=360,
189
+ template="plotly_white",
190
+ showlegend=False,
191
+ )
192
+
193
+ fig.show()
194
+
195
+ def report(self):
196
+ average_error = sum(self.errors) / self.size
197
+ mse = mean_squared_error(self.truths, self.guesses)
198
+ r2 = r2_score(self.truths, self.guesses) * 100
199
+ title = f"{self.title} results<br><b>Error:</b> ${average_error:,.2f} <b>MSE:</b> {mse:,.0f} <b>r²:</b> {r2:.1f}%"
200
+ self.error_trend_chart()
201
+ self.chart(title)
202
+
203
+ def run(self):
204
+ with ThreadPoolExecutor(max_workers=self.workers) as ex:
205
+ for title, guess, truth, error, color in tqdm(
206
+ ex.map(self.run_datapoint, range(self.size)), total=self.size
207
+ ):
208
+ self.titles.append(title)
209
+ self.guesses.append(guess)
210
+ self.truths.append(truth)
211
+ self.errors.append(error)
212
+ self.colors.append(color)
213
+ print(f"{COLOR_MAP[color]}${error:.0f} ", end="")
214
+ self.report()
215
+
216
+
217
+ def evaluate(function, data, size=DEFAULT_SIZE, workers=WORKERS):
218
+ Tester(function, data, size=size, workers=workers).run()
_hf_deploy/agents/frontier_agent.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from typing import List, Dict
4
+
5
+ from groq import Groq
6
+ from sentence_transformers import SentenceTransformer
7
+
8
+ from agents.agent import Agent
9
+ from free_config import GROQ_MODEL
10
+
11
+
12
+ class FrontierAgent(Agent):
13
+ name = "Frontier Agent"
14
+ color = Agent.BLUE
15
+
16
+ def __init__(self, collection):
17
+ """
18
+ Set up this instance by connecting to Groq, the Chroma datastore,
19
+ and the local sentence-transformer embedding model.
20
+ """
21
+ self.log("Initializing Frontier Agent")
22
+ self.client = Groq(api_key=os.environ["GROQ_API_KEY"])
23
+ self.model = GROQ_MODEL
24
+ self.log(f"Frontier Agent is setting up with Groq ({self.model})")
25
+ self.collection = collection
26
+ self.encoder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
27
+ self.log("Frontier Agent is ready")
28
+
29
+ def make_context(self, similars: List[str], prices: List[float]) -> str:
30
+ """
31
+ Create context that can be inserted into the prompt
32
+ """
33
+ message = "To provide some context, here are some other items that might be similar to the item you need to estimate.\n\n"
34
+ for similar, price in zip(similars, prices):
35
+ message += f"Potentially related product:\n{similar}\nPrice is ${price:.2f}\n\n"
36
+ return message
37
+
38
+ def messages_for(
39
+ self, description: str, similars: List[str], prices: List[float]
40
+ ) -> List[Dict[str, str]]:
41
+ """
42
+ Create the message list for the Groq chat completion call
43
+ """
44
+ message = f"Estimate the price of this product. Respond with the price only, no explanation.\n\n{description}\n\n"
45
+ message += self.make_context(similars, prices)
46
+ return [{"role": "user", "content": message}]
47
+
48
+ def find_similars(self, description: str):
49
+ """
50
+ Return a list of items similar to the given one by looking in the Chroma datastore
51
+ """
52
+ self.log(
53
+ "Frontier Agent is performing a RAG search of the Chroma datastore to find 5 similar products"
54
+ )
55
+ vector = self.encoder.encode([description])
56
+ results = self.collection.query(query_embeddings=vector.astype(float).tolist(), n_results=5)
57
+ documents = results["documents"][0][:]
58
+ prices = [m["price"] for m in results["metadatas"][0][:]]
59
+ self.log("Frontier Agent has found similar products")
60
+ return documents, prices
61
+
62
+ def get_price(self, s) -> float:
63
+ """
64
+ A utility that plucks a floating point number out of a string
65
+ """
66
+ s = s.replace("$", "").replace(",", "")
67
+ match = re.search(r"[-+]?\d*\.\d+|\d+", s)
68
+ return float(match.group()) if match else 0.0
69
+
70
+ def price(self, description: str) -> float:
71
+ """
72
+ Call Groq to estimate the price using RAG context from similar products.
73
+ """
74
+ documents, prices = self.find_similars(description)
75
+ self.log(
76
+ f"Frontier Agent is about to call Groq ({self.model}) with context including 5 similar products"
77
+ )
78
+ response = self.client.chat.completions.create(
79
+ model=self.model,
80
+ messages=self.messages_for(description, documents, prices),
81
+ temperature=0,
82
+ )
83
+ reply = response.choices[0].message.content
84
+ result = self.get_price(reply)
85
+ self.log(f"Frontier Agent completed - predicting ${result:.2f}")
86
+ return result
_hf_deploy/agents/items.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from datasets import Dataset, DatasetDict, load_dataset
3
+ from typing import Optional, Self
4
+
5
+
6
+ PREFIX = "Price is $"
7
+ QUESTION = "What does this cost to the nearest dollar?"
8
+
9
+
10
+ class Item(BaseModel):
11
+ """
12
+ An Item is a data-point of a Product with a Price
13
+ """
14
+
15
+ title: str
16
+ category: str
17
+ price: float
18
+ full: Optional[str] = None
19
+ weight: Optional[float] = None
20
+ summary: Optional[str] = None
21
+ prompt: Optional[str] = None
22
+ id: Optional[int] = None
23
+
24
+ def make_prompt(self, text: str):
25
+ self.prompt = f"{QUESTION}\n\n{text}\n\n{PREFIX}{round(self.price)}.00"
26
+
27
+ def test_prompt(self) -> str:
28
+ return self.prompt.split(PREFIX)[0] + PREFIX
29
+
30
+ def __repr__(self) -> str:
31
+ return f"<{self.title} = ${self.price}>"
32
+
33
+ @staticmethod
34
+ def push_to_hub(dataset_name: str, train: list[Self], val: list[Self], test: list[Self]):
35
+ """Push Item lists to HuggingFace Hub"""
36
+ DatasetDict(
37
+ {
38
+ "train": Dataset.from_list([item.model_dump() for item in train]),
39
+ "validation": Dataset.from_list([item.model_dump() for item in val]),
40
+ "test": Dataset.from_list([item.model_dump() for item in test]),
41
+ }
42
+ ).push_to_hub(dataset_name)
43
+
44
+ @classmethod
45
+ def from_hub(cls, dataset_name: str) -> tuple[list[Self], list[Self], list[Self]]:
46
+ """Load from HuggingFace Hub and reconstruct Items"""
47
+ ds = load_dataset(dataset_name)
48
+ return (
49
+ [cls.model_validate(row) for row in ds["train"]],
50
+ [cls.model_validate(row) for row in ds["validation"]],
51
+ [cls.model_validate(row) for row in ds["test"]],
52
+ )
_hf_deploy/agents/messaging_agent.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import requests
4
+ from litellm import completion
5
+
6
+ from agents.deals import Opportunity
7
+ from agents.agent import Agent
8
+ from free_config import MESSAGING_MODEL, PUSHOVER_CONFIGURED
9
+
10
+ pushover_url = "https://api.pushover.net/1/messages.json"
11
+
12
+
13
+ class MessagingAgent(Agent):
14
+ name = "Messaging Agent"
15
+ color = Agent.WHITE
16
+
17
+ def __init__(self):
18
+ """
19
+ Set up Pushover notifications (optional) and Groq for message crafting.
20
+ """
21
+ self.log("Messaging Agent is initializing")
22
+ self.pushover_user = os.getenv("PUSHOVER_USER", "")
23
+ self.pushover_token = os.getenv("PUSHOVER_TOKEN", "")
24
+ self.model = MESSAGING_MODEL
25
+ if PUSHOVER_CONFIGURED:
26
+ self.log("Messaging Agent has Pushover configured")
27
+ else:
28
+ self.log("Messaging Agent: Pushover not configured — alerts will be logged only")
29
+
30
+ def push(self, text):
31
+ """
32
+ Send a Push Notification using the Pushover API, or log if not configured.
33
+ """
34
+ if not PUSHOVER_CONFIGURED:
35
+ self.log(f"Deal alert (Pushover skipped): {text[:120]}...")
36
+ return
37
+ self.log("Messaging Agent is sending a push notification")
38
+ payload = {
39
+ "user": self.pushover_user,
40
+ "token": self.pushover_token,
41
+ "message": text,
42
+ "sound": "cashregister",
43
+ }
44
+ requests.post(pushover_url, data=payload)
45
+
46
+ def alert(self, opportunity: Opportunity):
47
+ """
48
+ Make an alert about the specified Opportunity
49
+ """
50
+ text = f"Deal Alert! Price=${opportunity.deal.price:.2f}, "
51
+ text += f"Estimate=${opportunity.estimate:.2f}, "
52
+ text += f"Discount=${opportunity.discount:.2f} :"
53
+ text += opportunity.deal.product_description[:10] + "... "
54
+ text += opportunity.deal.url
55
+ self.push(text)
56
+ self.log("Messaging Agent has completed")
57
+
58
+ def craft_message(
59
+ self, description: str, deal_price: float, estimated_true_value: float
60
+ ) -> str:
61
+ user_prompt = "Please summarize this great deal in 2-3 sentences to be sent as an exciting push notification alerting the user about this deal.\n"
62
+ user_prompt += f"Item Description: {description}\nOffered Price: {deal_price}\nEstimated true value: {estimated_true_value}"
63
+ user_prompt += "\n\nRespond only with the 2-3 sentence message which will be used to alert & excite the user about this deal"
64
+ response = completion(
65
+ model=self.model,
66
+ messages=[
67
+ {"role": "user", "content": user_prompt},
68
+ ],
69
+ )
70
+ return response.choices[0].message.content
71
+
72
+ def notify(self, description: str, deal_price: float, estimated_true_value: float, url: str):
73
+ """
74
+ Make an alert about the specified details
75
+ """
76
+ self.log(f"Messaging Agent is using Groq ({self.model}) to craft the message")
77
+ text = self.craft_message(description, deal_price, estimated_true_value)
78
+ self.push(text[:200] + "... " + url)
79
+ self.log("Messaging Agent has completed")
_hf_deploy/agents/neural_network_agent.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.agent import Agent
2
+ from agents.deep_neural_network import DeepNeuralNetworkInference
3
+
4
+
5
+ class NeuralNetworkAgent(Agent):
6
+ name = "Neural Network Agent"
7
+ color = Agent.MAGENTA
8
+
9
+ def __init__(self):
10
+ """
11
+ Initialize this object by loading in the saved model weights
12
+ and the SentenceTransformer vector encoding model
13
+ """
14
+ self.log("Neural Network Agent is initializing")
15
+ self.neural_network = DeepNeuralNetworkInference()
16
+ self.neural_network.setup()
17
+ self.neural_network.load("deep_neural_network.pth")
18
+ self.log("Neural Network Agent is ready and weights are loaded")
19
+
20
+ def price(self, description: str) -> float:
21
+ """
22
+ Use the Deep Neural Network to estimate the price of the described item
23
+ :param description: the product to be estimated
24
+ :return: the price as a float
25
+ """
26
+ self.log("Neural Network Agent is starting a prediction")
27
+ result = self.neural_network.inference(description)
28
+ self.log(f"Neural Network Agent completed - predicting ${result:.2f}")
29
+ return result
_hf_deploy/agents/planning_agent.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List
2
+ from agents.agent import Agent
3
+ from agents.deals import ScrapedDeal, DealSelection, Deal, Opportunity
4
+ from agents.scanner_agent import ScannerAgent
5
+ from agents.ensemble_agent import EnsembleAgent
6
+ from agents.messaging_agent import MessagingAgent
7
+
8
+
9
+ class PlanningAgent(Agent):
10
+
11
+ name = "Planning Agent"
12
+ color = Agent.GREEN
13
+ DEAL_THRESHOLD = 50
14
+
15
+ def __init__(self, collection):
16
+ """
17
+ Create instances of the 3 Agents that this planner coordinates across
18
+ """
19
+ self.log("Planning Agent is initializing")
20
+ self.scanner = ScannerAgent()
21
+ self.ensemble = EnsembleAgent(collection)
22
+ self.messenger = MessagingAgent()
23
+ self.log("Planning Agent is ready")
24
+
25
+ def run(self, deal: Deal) -> Opportunity:
26
+ """
27
+ Run the workflow for a particular deal
28
+ :param deal: the deal, summarized from an RSS scrape
29
+ :returns: an opportunity including the discount
30
+ """
31
+ self.log("Planning Agent is pricing up a potential deal")
32
+ estimate = self.ensemble.price(deal.product_description)
33
+ discount = estimate - deal.price
34
+ self.log(f"Planning Agent has processed a deal with discount ${discount:.2f}")
35
+ return Opportunity(deal=deal, estimate=estimate, discount=discount)
36
+
37
+ def plan(self, memory: List[str] = []) -> Optional[Opportunity]:
38
+ """
39
+ Run the full workflow:
40
+ 1. Use the ScannerAgent to find deals from RSS feeds
41
+ 2. Use the EnsembleAgent to estimate them
42
+ 3. Use the MessagingAgent to send a notification of deals
43
+ :param memory: a list of URLs that have been surfaced in the past
44
+ :return: an Opportunity if one was surfaced, otherwise None
45
+ """
46
+ self.log("Planning Agent is kicking off a run")
47
+ selection = self.scanner.scan(memory=memory)
48
+ if selection:
49
+ opportunities = [self.run(deal) for deal in selection.deals[:5]]
50
+ opportunities.sort(key=lambda opp: opp.discount, reverse=True)
51
+ best = opportunities[0]
52
+ self.log(f"Planning Agent has identified the best deal has discount ${best.discount:.2f}")
53
+ if best.discount > self.DEAL_THRESHOLD:
54
+ self.messenger.alert(best)
55
+ self.log("Planning Agent has completed a run")
56
+ return best if best.discount > self.DEAL_THRESHOLD else None
57
+ return None
_hf_deploy/agents/preprocessor.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+ from litellm import completion
5
+
6
+ from free_config import PREPROCESSOR_MODEL
7
+
8
+ load_dotenv(override=True)
9
+
10
+ DEFAULT_REASONING_EFFORT = "low" if "gpt-oss" in PREPROCESSOR_MODEL else None
11
+
12
+ SYSTEM_PROMPT = """Create a concise description of a product. Respond only in this format. Do not include part numbers.
13
+ Title: Rewritten short precise title
14
+ Category: eg Electronics
15
+ Brand: Brand name
16
+ Description: 1 sentence description
17
+ Details: 1 sentence on features"""
18
+
19
+
20
+ class Preprocessor:
21
+ def __init__(
22
+ self,
23
+ model_name=PREPROCESSOR_MODEL,
24
+ reasoning_effort=DEFAULT_REASONING_EFFORT,
25
+ base_url=None,
26
+ ):
27
+ self.total_input_tokens = 0
28
+ self.total_output_tokens = 0
29
+ self.total_cost = 0
30
+ self.model_name = model_name
31
+ self.reasoning_effort = reasoning_effort
32
+ self.base_url = base_url
33
+ if "ollama" in model_name and not base_url:
34
+ self.base_url = "http://localhost:11434"
35
+
36
+ def messages_for(self, text: str) -> list[dict]:
37
+ return [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": text}]
38
+
39
+ def preprocess(self, text: str) -> str:
40
+ messages = self.messages_for(text)
41
+ response = completion(
42
+ messages=messages,
43
+ model=self.model_name,
44
+ reasoning_effort=self.reasoning_effort,
45
+ api_base=self.base_url,
46
+ )
47
+ self.total_input_tokens += response.usage.prompt_tokens
48
+ self.total_output_tokens += response.usage.completion_tokens
49
+ self.total_cost += response._hidden_params["response_cost"]
50
+ return response.choices[0].message.content
_hf_deploy/agents/scanner_agent.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from typing import Optional, List
5
+
6
+ from groq import BadRequestError, Groq
7
+
8
+ from agents.deals import ScrapedDeal, DealSelection
9
+ from agents.agent import Agent
10
+ from free_config import GROQ_MODEL
11
+
12
+
13
+ class ScannerAgent(Agent):
14
+ MAX_DEALS_PER_REQUEST = 15
15
+
16
+ JSON_FORMAT_HINT = (
17
+ '\n\nRespond with JSON only, using this shape: '
18
+ '{"deals": [{"product_description": "...", "price": 99.99, "url": "https://..."}]}'
19
+ '\nThe JSON must be valid: no double-quote characters inside product_description '
20
+ '(write "6.9 inches" instead of 6.9"). No trailing commas. '
21
+ 'Close the deals array with a single ] and the object with a single }.'
22
+ )
23
+
24
+ SYSTEM_PROMPT = """You identify and summarize the 5 most detailed deals from a list, by selecting deals that have the most detailed, high quality description and the most clear price.
25
+ You should provide the price as a number derived from the description. If the price of a deal isn't clear, do not include that deal in your response.
26
+ Most important is that you respond with the 5 deals that have the most detailed product description with price. It's not important to mention the terms of the deal; most important is a thorough description of the product.
27
+ Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price.
28
+ Never use the double-quote character inside product_description text. Spell out inch measurements as words, e.g. "55 inches" not 55".
29
+ """
30
+
31
+ USER_PROMPT_PREFIX = """Respond with the most promising 5 deals from this list, selecting those which have the most detailed, high quality product description and a clear price that is greater than 0.
32
+ You should rephrase the description to be a summary of the product itself, not the terms of the deal.
33
+ Remember to respond with a short paragraph of text in the product_description field for each of the 5 items that you select.
34
+ Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price.
35
+
36
+ Deals:
37
+
38
+ """
39
+
40
+ USER_PROMPT_SUFFIX = "\n\nInclude exactly 5 deals, no more."
41
+
42
+ name = "Scanner Agent"
43
+ color = Agent.CYAN
44
+
45
+ def __init__(self):
46
+ self.log("Scanner Agent is initializing")
47
+ self.client = Groq(api_key=os.environ["GROQ_API_KEY"])
48
+ self.model = GROQ_MODEL
49
+ self.log(f"Scanner Agent is ready (Groq / {self.model})")
50
+
51
+ def fetch_deals(self, memory) -> List[ScrapedDeal]:
52
+ """
53
+ Look up deals published on RSS feeds
54
+ Return any new deals that are not already in the memory provided
55
+ """
56
+ self.log("Scanner Agent is about to fetch deals from RSS feed")
57
+ urls = [opp.deal.url for opp in memory]
58
+ scraped = ScrapedDeal.fetch()
59
+ result = [scrape for scrape in scraped if scrape.url not in urls]
60
+ self.log(f"Scanner Agent received {len(result)} deals not already scraped")
61
+ return result
62
+
63
+ def make_user_prompt(self, scraped) -> str:
64
+ """
65
+ Create a user prompt based on the scraped deals provided
66
+ """
67
+ user_prompt = self.USER_PROMPT_PREFIX
68
+ user_prompt += "\n\n".join(
69
+ [scrape.describe() for scrape in scraped[: self.MAX_DEALS_PER_REQUEST]]
70
+ )
71
+ user_prompt += self.USER_PROMPT_SUFFIX
72
+ return user_prompt
73
+
74
+ @staticmethod
75
+ def _repair_deals_json(text: str) -> str:
76
+ text = text.strip()
77
+ text = re.sub(r'(\d+(?:\.\d+)?)"(\s+(?=[a-zA-Z0-9]))', r"\1 inches\2", text)
78
+ text = re.sub(r'("url"\s*:\s*"[^"]*")\s*\]', r"\1\n }", text)
79
+ text = re.sub(
80
+ r'("url"\s*:\s*"[^"]*")\s*\}\s*\n\s*\]\s*\n\s*\}\}',
81
+ r"\1\n }\n ]\n}",
82
+ text,
83
+ )
84
+ text = re.sub(r"(\])\s*\]\s*\}\s*\}", r"\1}", text)
85
+ text = re.sub(r"(\])\s*\]\s*\}", r"\1}", text)
86
+ text = re.sub(r"\}\s*\}\s*$", r"}", text)
87
+ return text
88
+
89
+ def _parse_deals_json(self, content: str) -> DealSelection:
90
+ last_error = None
91
+ for candidate in (content, self._repair_deals_json(content)):
92
+ try:
93
+ result = DealSelection.model_validate(json.loads(candidate))
94
+ result.deals = [deal for deal in result.deals if deal.price > 0]
95
+ return result
96
+ except (json.JSONDecodeError, ValueError) as exc:
97
+ last_error = exc
98
+ raise ValueError(f"Could not parse deals JSON: {last_error}")
99
+
100
+ @staticmethod
101
+ def _extract_failed_generation(exc: BadRequestError) -> Optional[str]:
102
+ body = getattr(exc, "body", None)
103
+ if isinstance(body, dict):
104
+ return body.get("error", {}).get("failed_generation")
105
+ return None
106
+
107
+ def _call_groq(self, user_prompt: str) -> DealSelection:
108
+ messages = [
109
+ {"role": "system", "content": self.SYSTEM_PROMPT + self.JSON_FORMAT_HINT},
110
+ {"role": "user", "content": user_prompt},
111
+ ]
112
+ try:
113
+ response = self.client.chat.completions.create(
114
+ model=self.model,
115
+ messages=messages,
116
+ response_format={"type": "json_object"},
117
+ temperature=0,
118
+ )
119
+ return self._parse_deals_json(response.choices[0].message.content)
120
+ except BadRequestError as exc:
121
+ failed = self._extract_failed_generation(exc)
122
+ if failed:
123
+ self.log("Scanner Agent repairing malformed JSON from Groq response")
124
+ return self._parse_deals_json(failed)
125
+ raise
126
+
127
+ def scan(self, memory: List[str] = []) -> Optional[DealSelection]:
128
+ """
129
+ Call Groq to provide a high potential list of deals with good descriptions and prices
130
+ :param memory: a list of URLs representing deals already raised
131
+ :return: a selection of good deals, or None if there aren't any
132
+ """
133
+ scraped = self.fetch_deals(memory)
134
+ if not scraped:
135
+ return None
136
+ user_prompt = self.make_user_prompt(scraped)
137
+ self.log(f"Scanner Agent is calling Groq ({self.model})")
138
+ try:
139
+ result = self._call_groq(user_prompt)
140
+ except Exception as exc:
141
+ self.log(f"Scanner Agent failed to parse Groq response: {exc}")
142
+ return None
143
+ self.log(
144
+ f"Scanner Agent received {len(result.deals)} selected deals with price>0 from Groq"
145
+ )
146
+ return result
147
+
148
+ def test_scan(self, memory: List[str] = []) -> Optional[DealSelection]:
149
+ """
150
+ Return a test DealSelection, to be used during testing
151
+ """
152
+ results = {
153
+ "deals": [
154
+ {
155
+ "product_description": "The Hisense R6 Series 55R6030N is a 55-inch 4K UHD Roku Smart TV that offers stunning picture quality with 3840x2160 resolution. It features Dolby Vision HDR and HDR10 compatibility, ensuring a vibrant and dynamic viewing experience. The TV runs on Roku's operating system, allowing easy access to streaming services and voice control compatibility with Google Assistant and Alexa. With three HDMI ports available, connecting multiple devices is simple and efficient.",
156
+ "price": 178,
157
+ "url": "https://www.dealnews.com/products/Hisense/Hisense-R6-Series-55-R6030-N-55-4-K-UHD-Roku-Smart-TV/484824.html?iref=rss-c142",
158
+ },
159
+ {
160
+ "product_description": "The Poly Studio P21 is a 21.5-inch LED personal meeting display designed specifically for remote work and video conferencing. With a native resolution of 1080p, it provides crystal-clear video quality, featuring a privacy shutter and stereo speakers. This display includes a 1080p webcam with manual pan, tilt, and zoom control, along with an ambient light sensor to adjust the vanity lighting as needed. It also supports 5W wireless charging for mobile devices, making it an all-in-one solution for home offices.",
161
+ "price": 30,
162
+ "url": "https://www.dealnews.com/products/Poly-Studio-P21-21-5-1080-p-LED-Personal-Meeting-Display/378335.html?iref=rss-c39",
163
+ },
164
+ {
165
+ "product_description": "The Lenovo IdeaPad Slim 5 laptop is powered by a 7th generation AMD Ryzen 5 8645HS 6-core CPU, offering efficient performance for multitasking and demanding applications. It features a 16-inch touch display with a resolution of 1920x1080, ensuring bright and vivid visuals. Accompanied by 16GB of RAM and a 512GB SSD, the laptop provides ample speed and storage for all your files. This model is designed to handle everyday tasks with ease while delivering an enjoyable user experience.",
166
+ "price": 446,
167
+ "url": "https://www.dealnews.com/products/Lenovo/Lenovo-Idea-Pad-Slim-5-7-th-Gen-Ryzen-5-16-Touch-Laptop/485068.html?iref=rss-c39",
168
+ },
169
+ {
170
+ "product_description": "The Dell G15 gaming laptop is equipped with a 6th-generation AMD Ryzen 5 7640HS 6-Core CPU, providing powerful performance for gaming and content creation. It features a 15.6-inch 1080p display with a 120Hz refresh rate, allowing for smooth and responsive gameplay. With 16GB of RAM and a substantial 1TB NVMe M.2 SSD, this laptop ensures speedy performance and plenty of storage for games and applications. Additionally, it includes the Nvidia GeForce RTX 3050 GPU for enhanced graphics and gaming experiences.",
171
+ "price": 650,
172
+ "url": "https://www.dealnews.com/products/Dell/Dell-G15-Ryzen-5-15-6-Gaming-Laptop-w-Nvidia-RTX-3050/485067.html?iref=rss-c39",
173
+ },
174
+ ]
175
+ }
176
+ return DealSelection(**results)
_hf_deploy/agents/specialist_agent.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+
4
+ from agents.agent import Agent
5
+ from free_config import GROQ_MODEL, USE_MODAL_SPECIALIST
6
+
7
+ QUESTION = "What does this cost to the nearest dollar?"
8
+ PREFIX = "Price is $"
9
+
10
+
11
+ class SpecialistAgent(Agent):
12
+ """
13
+ Prices products using either:
14
+ 1. Fine-tuned Llama on Modal (best quality, uses GPU credits), or
15
+ 2. Groq fallback (free — same prompt format as the fine-tuned model)
16
+
17
+ Set USE_MODAL_SPECIALIST=true only if you have Modal credits to spare.
18
+ """
19
+
20
+ name = "Specialist Agent"
21
+ color = Agent.RED
22
+
23
+ def __init__(self):
24
+ self.pricer = None
25
+ self.groq_client = None
26
+ self.mode = "off"
27
+ self.model = GROQ_MODEL
28
+
29
+ if USE_MODAL_SPECIALIST:
30
+ self._try_modal()
31
+
32
+ if self.mode == "off" and os.getenv("GROQ_API_KEY"):
33
+ from groq import Groq
34
+
35
+ self.groq_client = Groq(api_key=os.environ["GROQ_API_KEY"])
36
+ self.mode = "groq"
37
+ self.log(f"Specialist Agent using Groq fallback ({self.model}) — no Modal GPU needed")
38
+
39
+ if self.mode == "off":
40
+ self.log("Specialist Agent disabled — ensemble will use Frontier + Neural Network only")
41
+
42
+ def _try_modal(self):
43
+ try:
44
+ import modal
45
+
46
+ self.log("Specialist Agent is initializing - connecting to Modal")
47
+ Pricer = modal.Cls.from_name("pricer-service", "Pricer")
48
+ self.pricer = Pricer()
49
+ self.mode = "modal"
50
+ self.log("Specialist Agent connected to Modal pricer-service")
51
+ except Exception as exc:
52
+ self.log(f"Modal unavailable ({exc}) — will try Groq fallback if configured")
53
+
54
+ @property
55
+ def available(self) -> bool:
56
+ return self.mode != "off"
57
+
58
+ @staticmethod
59
+ def _parse_price(text: str) -> float:
60
+ if PREFIX in text:
61
+ text = text.split(PREFIX, 1)[1]
62
+ text = text.replace("$", "").replace(",", "")
63
+ match = re.search(r"[-+]?\d*\.\d+|\d+", text)
64
+ return float(match.group()) if match else 0.0
65
+
66
+ def _price_with_groq(self, description: str) -> float:
67
+ prompt = f"{QUESTION}\n\n{description}\n\n{PREFIX}"
68
+ response = self.groq_client.chat.completions.create(
69
+ model=self.model,
70
+ messages=[{"role": "user", "content": prompt}],
71
+ temperature=0,
72
+ max_tokens=8,
73
+ )
74
+ reply = response.choices[0].message.content or ""
75
+ return self._parse_price(reply)
76
+
77
+ def price(self, description: str) -> float:
78
+ if self.mode == "modal":
79
+ self.log("Specialist Agent is calling remote fine-tuned model on Modal")
80
+ result = self.pricer.price.remote(description)
81
+ self.log(f"Specialist Agent completed - predicting ${result:.2f}")
82
+ return result
83
+
84
+ if self.mode == "groq":
85
+ self.log(f"Specialist Agent is calling Groq ({self.model})")
86
+ result = self._price_with_groq(description)
87
+ self.log(f"Specialist Agent completed - predicting ${result:.2f}")
88
+ return result
89
+
90
+ return 0.0
_hf_deploy/app.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces entry point.
3
+
4
+ Upload the entire week8/ folder into the Space repo root (this file becomes app.py),
5
+ or copy week8/* into the Space and rename/move as needed.
6
+
7
+ Required Space secrets: GROQ_API_KEY, HF_TOKEN, USE_MODAL_SPECIALIST=false
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # When week8 is the Space root, agents/ and deal_agent_framework.py are siblings.
15
+ ROOT = Path(__file__).resolve().parent
16
+ if str(ROOT) not in sys.path:
17
+ sys.path.insert(0, str(ROOT))
18
+
19
+ os.environ.setdefault("GRADIO_SERVER_NAME", "0.0.0.0")
20
+ os.environ.setdefault("PRICER_PREPROCESSOR_MODEL", "groq/llama-3.1-8b-instant")
21
+
22
+ from price_is_right import App
23
+
24
+ if __name__ == "__main__":
25
+ App().run()
_hf_deploy/deal_agent_framework.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import logging
4
+ import json
5
+ from typing import List
6
+ from dotenv import load_dotenv
7
+ import chromadb
8
+ from agents.planning_agent import PlanningAgent
9
+ from agents.deals import Opportunity
10
+ from sklearn.manifold import TSNE
11
+ import numpy as np
12
+
13
+ load_dotenv(override=True)
14
+
15
+ # Colors for logging
16
+ BG_BLUE = "\033[44m"
17
+ WHITE = "\033[37m"
18
+ RESET = "\033[0m"
19
+
20
+ # Colors for plot
21
+ CATEGORIES = [
22
+ "Appliances",
23
+ "Automotive",
24
+ "Cell_Phones_and_Accessories",
25
+ "Electronics",
26
+ "Musical_Instruments",
27
+ "Office_Products",
28
+ "Tools_and_Home_Improvement",
29
+ "Toys_and_Games",
30
+ ]
31
+ COLORS = ["red", "blue", "brown", "orange", "yellow", "green", "purple", "cyan"]
32
+
33
+
34
+ def init_logging():
35
+ root = logging.getLogger()
36
+ root.setLevel(logging.INFO)
37
+
38
+ handler = logging.StreamHandler(sys.stdout)
39
+ handler.setLevel(logging.INFO)
40
+ formatter = logging.Formatter(
41
+ "[%(asctime)s] [Agents] [%(levelname)s] %(message)s",
42
+ datefmt="%Y-%m-%d %H:%M:%S %z",
43
+ )
44
+ handler.setFormatter(formatter)
45
+ root.addHandler(handler)
46
+
47
+
48
+ class DealAgentFramework:
49
+ DB = "products_vectorstore"
50
+ MEMORY_FILENAME = "memory.json"
51
+
52
+ def __init__(self):
53
+ init_logging()
54
+ client = chromadb.PersistentClient(path=self.DB)
55
+ self.memory = self.read_memory()
56
+ self.collection = client.get_or_create_collection("products")
57
+ self.planner = None
58
+
59
+ def init_agents_as_needed(self):
60
+ if not self.planner:
61
+ self.log("Initializing Agent Framework")
62
+ self.planner = PlanningAgent(self.collection)
63
+ self.log("Agent Framework is ready")
64
+
65
+ def read_memory(self) -> List[Opportunity]:
66
+ if os.path.exists(self.MEMORY_FILENAME):
67
+ with open(self.MEMORY_FILENAME, "r") as file:
68
+ data = json.load(file)
69
+ opportunities = [Opportunity(**item) for item in data]
70
+ return opportunities
71
+ return []
72
+
73
+ def write_memory(self) -> None:
74
+ data = [opportunity.model_dump() for opportunity in self.memory]
75
+ with open(self.MEMORY_FILENAME, "w") as file:
76
+ json.dump(data, file, indent=2)
77
+
78
+ @classmethod
79
+ def reset_memory(cls) -> None:
80
+ data = []
81
+ if os.path.exists(cls.MEMORY_FILENAME):
82
+ with open(cls.MEMORY_FILENAME, "r") as file:
83
+ data = json.load(file)
84
+ truncated = data[:2]
85
+ with open(cls.MEMORY_FILENAME, "w") as file:
86
+ json.dump(truncated, file, indent=2)
87
+
88
+ def log(self, message: str):
89
+ text = BG_BLUE + WHITE + "[Agent Framework] " + message + RESET
90
+ logging.info(text)
91
+
92
+ def run(self) -> List[Opportunity]:
93
+ self.init_agents_as_needed()
94
+ logging.info("Kicking off Planning Agent")
95
+ result = self.planner.plan(memory=self.memory)
96
+ logging.info(f"Planning Agent has completed and returned: {result}")
97
+ if result:
98
+ self.memory.append(result)
99
+ self.write_memory()
100
+ return self.memory
101
+
102
+ @classmethod
103
+ def get_plot_data(cls, max_datapoints=2000):
104
+ client = chromadb.PersistentClient(path=cls.DB)
105
+ collection = client.get_or_create_collection("products")
106
+ result = collection.get(
107
+ include=["embeddings", "documents", "metadatas"], limit=max_datapoints
108
+ )
109
+ vectors = np.array(result["embeddings"])
110
+ documents = result["documents"]
111
+ categories = [metadata["category"] for metadata in result["metadatas"]]
112
+ colors = [COLORS[CATEGORIES.index(c)] for c in categories]
113
+ tsne = TSNE(n_components=3, random_state=42, n_jobs=-1)
114
+ reduced_vectors = tsne.fit_transform(vectors)
115
+ return documents, reduced_vectors, colors
116
+
117
+
118
+ if __name__ == "__main__":
119
+ DealAgentFramework().run()
_hf_deploy/env_utils.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load .env from repo root or week8 folder."""
2
+
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ WEEK8_DIR = Path(__file__).resolve().parent
8
+ REPO_ROOT = WEEK8_DIR.parent
9
+
10
+
11
+ def load_project_env() -> None:
12
+ for path in (REPO_ROOT / ".env", WEEK8_DIR / ".env"):
13
+ if path.exists():
14
+ load_dotenv(path, override=True)