Spaces:
Running on Zero
Running on Zero
File size: 10,159 Bytes
58bd26a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import json
import os
import re
from typing import Optional, List
from groq import BadRequestError, Groq
from agents.deals import ScrapedDeal, DealSelection
from agents.agent import Agent
from free_config import GROQ_MODEL
class ScannerAgent(Agent):
MAX_DEALS_PER_REQUEST = 15
JSON_FORMAT_HINT = (
'\n\nRespond with JSON only, using this shape: '
'{"deals": [{"product_description": "...", "price": 99.99, "url": "https://..."}]}'
'\nThe JSON must be valid: no double-quote characters inside product_description '
'(write "6.9 inches" instead of 6.9"). No trailing commas. '
'Close the deals array with a single ] and the object with a single }.'
)
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.
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.
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.
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.
Never use the double-quote character inside product_description text. Spell out inch measurements as words, e.g. "55 inches" not 55".
"""
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.
You should rephrase the description to be a summary of the product itself, not the terms of the deal.
Remember to respond with a short paragraph of text in the product_description field for each of the 5 items that you select.
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.
Deals:
"""
USER_PROMPT_SUFFIX = "\n\nInclude exactly 5 deals, no more."
name = "Scanner Agent"
color = Agent.CYAN
def __init__(self):
self.log("Scanner Agent is initializing")
self.client = Groq(api_key=os.environ["GROQ_API_KEY"])
self.model = GROQ_MODEL
self.log(f"Scanner Agent is ready (Groq / {self.model})")
def fetch_deals(self, memory) -> List[ScrapedDeal]:
"""
Look up deals published on RSS feeds
Return any new deals that are not already in the memory provided
"""
self.log("Scanner Agent is about to fetch deals from RSS feed")
urls = [opp.deal.url for opp in memory]
scraped = ScrapedDeal.fetch()
result = [scrape for scrape in scraped if scrape.url not in urls]
self.log(f"Scanner Agent received {len(result)} deals not already scraped")
return result
def make_user_prompt(self, scraped) -> str:
"""
Create a user prompt based on the scraped deals provided
"""
user_prompt = self.USER_PROMPT_PREFIX
user_prompt += "\n\n".join(
[scrape.describe() for scrape in scraped[: self.MAX_DEALS_PER_REQUEST]]
)
user_prompt += self.USER_PROMPT_SUFFIX
return user_prompt
@staticmethod
def _repair_deals_json(text: str) -> str:
text = text.strip()
text = re.sub(r'(\d+(?:\.\d+)?)"(\s+(?=[a-zA-Z0-9]))', r"\1 inches\2", text)
text = re.sub(r'("url"\s*:\s*"[^"]*")\s*\]', r"\1\n }", text)
text = re.sub(
r'("url"\s*:\s*"[^"]*")\s*\}\s*\n\s*\]\s*\n\s*\}\}',
r"\1\n }\n ]\n}",
text,
)
text = re.sub(r"(\])\s*\]\s*\}\s*\}", r"\1}", text)
text = re.sub(r"(\])\s*\]\s*\}", r"\1}", text)
text = re.sub(r"\}\s*\}\s*$", r"}", text)
return text
def _parse_deals_json(self, content: str) -> DealSelection:
last_error = None
for candidate in (content, self._repair_deals_json(content)):
try:
result = DealSelection.model_validate(json.loads(candidate))
result.deals = [deal for deal in result.deals if deal.price > 0]
return result
except (json.JSONDecodeError, ValueError) as exc:
last_error = exc
raise ValueError(f"Could not parse deals JSON: {last_error}")
@staticmethod
def _extract_failed_generation(exc: BadRequestError) -> Optional[str]:
body = getattr(exc, "body", None)
if isinstance(body, dict):
return body.get("error", {}).get("failed_generation")
return None
def _call_groq(self, user_prompt: str) -> DealSelection:
messages = [
{"role": "system", "content": self.SYSTEM_PROMPT + self.JSON_FORMAT_HINT},
{"role": "user", "content": user_prompt},
]
try:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
response_format={"type": "json_object"},
temperature=0,
)
return self._parse_deals_json(response.choices[0].message.content)
except BadRequestError as exc:
failed = self._extract_failed_generation(exc)
if failed:
self.log("Scanner Agent repairing malformed JSON from Groq response")
return self._parse_deals_json(failed)
raise
def scan(self, memory: List[str] = []) -> Optional[DealSelection]:
"""
Call Groq to provide a high potential list of deals with good descriptions and prices
:param memory: a list of URLs representing deals already raised
:return: a selection of good deals, or None if there aren't any
"""
scraped = self.fetch_deals(memory)
if not scraped:
return None
user_prompt = self.make_user_prompt(scraped)
self.log(f"Scanner Agent is calling Groq ({self.model})")
try:
result = self._call_groq(user_prompt)
except Exception as exc:
self.log(f"Scanner Agent failed to parse Groq response: {exc}")
return None
self.log(
f"Scanner Agent received {len(result.deals)} selected deals with price>0 from Groq"
)
return result
def test_scan(self, memory: List[str] = []) -> Optional[DealSelection]:
"""
Return a test DealSelection, to be used during testing
"""
results = {
"deals": [
{
"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.",
"price": 178,
"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",
},
{
"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.",
"price": 30,
"url": "https://www.dealnews.com/products/Poly-Studio-P21-21-5-1080-p-LED-Personal-Meeting-Display/378335.html?iref=rss-c39",
},
{
"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.",
"price": 446,
"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",
},
{
"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.",
"price": 650,
"url": "https://www.dealnews.com/products/Dell/Dell-G15-Ryzen-5-15-6-Gaming-Laptop-w-Nvidia-RTX-3050/485067.html?iref=rss-c39",
},
]
}
return DealSelection(**results)
|