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)