pycdc 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycdc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pycdc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycdc
3
+ Version: 0.1.0
4
+ Summary: Please Your Customer Direct Connect — search the web for CRM gift ideas and client appreciation strategies
5
+ Project-URL: Homepage, https://github.com/brnv/pycdc
6
+ Author-email: Artem <brnv@canva.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: appreciation,client-relations,crm,customer,direct-connect,gifts
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx>=0.27
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pycdc
20
+
21
+ **P**lease **Y**our **C**ustomer **D**irect **C**onnect — a library to search the web and get gift ideas for CRM client appreciation.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install pycdc
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ from pycdc import GiftFinder, CustomerProfile, Occasion, Budget
33
+
34
+ finder = GiftFinder()
35
+
36
+ # Simple search
37
+ ideas = finder.find(occasion=Occasion.RENEWAL, budget=Budget.STANDARD)
38
+
39
+ # Customer-aware recommendations
40
+ customer = CustomerProfile(
41
+ name="Sarah Chen",
42
+ company="Acme Corp",
43
+ deal_value=50_000,
44
+ interests=["coffee", "hiking", "tech"],
45
+ relationship_length_months=18,
46
+ )
47
+ recs = finder.recommend(customer, Occasion.ANNIVERSARY)
48
+ for r in recs:
49
+ print(f"{r.name} ({r.price_range}) — {r.personalization_tip}")
50
+
51
+ # Full gift strategy
52
+ print(finder.summary_for(customer))
53
+ ```
54
+
55
+ ## CLI
56
+
57
+ ```bash
58
+ pycdc search "tech" --budget premium
59
+ pycdc search --occasion holiday --budget modest --json
60
+ pycdc recommend --name "Sarah Chen" --company "Acme" --deal-value 50000 --interests coffee hiking
61
+ pycdc occasions
62
+ pycdc budgets
63
+ ```
64
+
65
+ ## License
66
+
67
+ MIT
pycdc-0.1.0/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # pycdc
2
+
3
+ **P**lease **Y**our **C**ustomer **D**irect **C**onnect — a library to search the web and get gift ideas for CRM client appreciation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pycdc
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from pycdc import GiftFinder, CustomerProfile, Occasion, Budget
15
+
16
+ finder = GiftFinder()
17
+
18
+ # Simple search
19
+ ideas = finder.find(occasion=Occasion.RENEWAL, budget=Budget.STANDARD)
20
+
21
+ # Customer-aware recommendations
22
+ customer = CustomerProfile(
23
+ name="Sarah Chen",
24
+ company="Acme Corp",
25
+ deal_value=50_000,
26
+ interests=["coffee", "hiking", "tech"],
27
+ relationship_length_months=18,
28
+ )
29
+ recs = finder.recommend(customer, Occasion.ANNIVERSARY)
30
+ for r in recs:
31
+ print(f"{r.name} ({r.price_range}) — {r.personalization_tip}")
32
+
33
+ # Full gift strategy
34
+ print(finder.summary_for(customer))
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ ```bash
40
+ pycdc search "tech" --budget premium
41
+ pycdc search --occasion holiday --budget modest --json
42
+ pycdc recommend --name "Sarah Chen" --company "Acme" --deal-value 50000 --interests coffee hiking
43
+ pycdc occasions
44
+ pycdc budgets
45
+ ```
46
+
47
+ ## License
48
+
49
+ MIT
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pycdc"
7
+ version = "0.1.0"
8
+ description = "Please Your Customer Direct Connect — search the web for CRM gift ideas and client appreciation strategies"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Artem", email = "brnv@canva.com" },
14
+ ]
15
+ keywords = ["crm", "gifts", "customer", "appreciation", "direct-connect", "client-relations"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Office/Business :: Financial :: Point-Of-Sale",
22
+ ]
23
+ dependencies = [
24
+ "httpx>=0.27",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/brnv/pycdc"
29
+
30
+ [project.scripts]
31
+ pycdc = "pycdc.cli:main"
@@ -0,0 +1,8 @@
1
+ """pycdc: Please Your Customer Direct Connect — CRM gift idea finder."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .client import GiftFinder
6
+ from .models import GiftIdea, Occasion, Budget, CustomerProfile
7
+
8
+ __all__ = ["GiftFinder", "GiftIdea", "Occasion", "Budget", "CustomerProfile"]
@@ -0,0 +1,95 @@
1
+ """Built-in gift catalog with curated ideas by occasion and budget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import GiftIdea, Occasion, Budget
6
+
7
+ # Curated gift catalog
8
+ CATALOG: list[GiftIdea] = [
9
+ # Token gifts
10
+ GiftIdea("Handwritten Thank You Card", "Premium card with personal note", "$5-10",
11
+ Budget.TOKEN, Occasion.THANK_YOU, category="stationery",
12
+ personalization_tip="Reference a specific project or win you shared"),
13
+ GiftIdea("Custom Sticker Pack", "Branded fun stickers for their laptop", "$8-12",
14
+ Budget.TOKEN, Occasion.ONBOARDING, category="swag",
15
+ personalization_tip="Include their company logo alongside yours"),
16
+ GiftIdea("Artisan Coffee Sampler", "4-pack of single-origin coffee", "$12-15",
17
+ Budget.TOKEN, Occasion.REFERRAL, category="food",
18
+ personalization_tip="Add a note: 'Thanks a latte for the referral'"),
19
+
20
+ # Modest gifts
21
+ GiftIdea("Desk Plant Kit", "Low-maintenance succulent in branded pot", "$20-35",
22
+ Budget.MODEST, Occasion.ONBOARDING, category="office",
23
+ personalization_tip="Choose a plant that matches their office aesthetic"),
24
+ GiftIdea("Book — Their Industry", "Bestseller relevant to their field", "$15-30",
25
+ Budget.MODEST, Occasion.MILESTONE, category="books",
26
+ personalization_tip="Write a note inside the cover about why you picked it"),
27
+ GiftIdea("Gourmet Cookie Box", "Assorted artisan cookies, 12-pack", "$25-40",
28
+ Budget.MODEST, Occasion.HOLIDAY, category="food",
29
+ personalization_tip="Check for dietary restrictions first"),
30
+ GiftIdea("Charity Donation in Their Name", "Donation to a cause they care about", "$25-50",
31
+ Budget.MODEST, Occasion.THANK_YOU, category="charity",
32
+ personalization_tip="Pick a cause related to their stated values or interests"),
33
+
34
+ # Standard gifts
35
+ GiftIdea("Premium Bluetooth Speaker", "Compact high-quality speaker", "$60-100",
36
+ Budget.STANDARD, Occasion.CLOSING_DEAL, category="tech",
37
+ personalization_tip="Engrave their initials or a short message"),
38
+ GiftIdea("Wine & Cheese Hamper", "Curated selection with tasting notes", "$75-120",
39
+ Budget.STANDARD, Occasion.ANNIVERSARY, category="food",
40
+ personalization_tip="Include wines from their home region if possible"),
41
+ GiftIdea("Spa Gift Card", "Relaxation package at a local spa", "$80-150",
42
+ Budget.STANDARD, Occasion.RENEWAL, category="experience",
43
+ personalization_tip="'Celebrating another year of partnership — you deserve a break'"),
44
+ GiftIdea("Custom Illustration", "Commissioned portrait or office artwork", "$50-120",
45
+ Budget.STANDARD, Occasion.MILESTONE, category="art",
46
+ personalization_tip="Commission art of their office building or team mascot"),
47
+
48
+ # Premium gifts
49
+ GiftIdea("Weekend Getaway Voucher", "Hotel voucher for a weekend escape", "$200-400",
50
+ Budget.PREMIUM, Occasion.CLOSING_DEAL, category="experience",
51
+ personalization_tip="Pick a destination near their city or a place they've mentioned"),
52
+ GiftIdea("Noise-Cancelling Headphones", "Sony or Bose premium headphones", "$250-350",
53
+ Budget.PREMIUM, Occasion.RENEWAL, category="tech",
54
+ personalization_tip="Great for clients who travel frequently"),
55
+ GiftIdea("Leather Portfolio Set", "Handcrafted leather notebook + pen", "$150-300",
56
+ Budget.PREMIUM, Occasion.PROMOTION, category="luxury",
57
+ personalization_tip="Monogram with their initials"),
58
+
59
+ # VIP gifts
60
+ GiftIdea("Private Dining Experience", "Chef's table dinner for two", "$500-800",
61
+ Budget.VIP, Occasion.ANNIVERSARY, category="experience",
62
+ personalization_tip="Invite them personally — make it a relationship-building event"),
63
+ GiftIdea("Custom Watch Engraving", "Premium watch with personalized engraving", "$500-1500",
64
+ Budget.VIP, Occasion.MILESTONE, category="luxury",
65
+ personalization_tip="Engrave the date of your first deal together"),
66
+ GiftIdea("Team Offsite Sponsorship", "Fund a team activity for their company", "$1000+",
67
+ Budget.VIP, Occasion.RENEWAL, category="experience",
68
+ personalization_tip="Offer to cover an escape room, cooking class, or wine tasting"),
69
+ ]
70
+
71
+
72
+ def search_catalog(*, occasion: Occasion | None = None, budget: Budget | None = None,
73
+ query: str = "", interests: list[str] | None = None) -> list[GiftIdea]:
74
+ """Search the built-in catalog."""
75
+ results = list(CATALOG)
76
+
77
+ if occasion:
78
+ results = [g for g in results if g.occasion == occasion]
79
+ if budget:
80
+ results = [g for g in results if g.budget == budget]
81
+ if query:
82
+ q = query.lower()
83
+ results = [g for g in results if q in g.name.lower() or q in g.description.lower()
84
+ or q in g.category.lower()]
85
+ if interests:
86
+ interest_set = {i.lower() for i in interests}
87
+ scored = []
88
+ for g in results:
89
+ text = f"{g.name} {g.description} {g.category}".lower()
90
+ matches = sum(1 for i in interest_set if i in text)
91
+ g.score = matches / len(interest_set) if interest_set else 0
92
+ scored.append(g)
93
+ results = sorted(scored, key=lambda g: g.score, reverse=True)
94
+
95
+ return results
@@ -0,0 +1,88 @@
1
+ """CLI for pycdc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> None:
11
+ parser = argparse.ArgumentParser(
12
+ prog="pycdc",
13
+ description="Please Your Customer Direct Connect — find CRM gift ideas",
14
+ )
15
+ sub = parser.add_subparsers(dest="command")
16
+
17
+ # Search
18
+ search_p = sub.add_parser("search", help="Search for gift ideas")
19
+ search_p.add_argument("query", nargs="?", default="", help="Search keyword")
20
+ search_p.add_argument("--occasion", choices=[o.value for o in _occasions()])
21
+ search_p.add_argument("--budget", choices=[b.value for b in _budgets()])
22
+ search_p.add_argument("--no-web", action="store_true", help="Skip web search")
23
+ search_p.add_argument("--json", action="store_true")
24
+
25
+ # Recommend
26
+ rec_p = sub.add_parser("recommend", help="Get recommendations for a customer")
27
+ rec_p.add_argument("--name", required=True)
28
+ rec_p.add_argument("--company", default="")
29
+ rec_p.add_argument("--deal-value", type=float, default=0)
30
+ rec_p.add_argument("--interests", nargs="*", default=[])
31
+ rec_p.add_argument("--occasion", default="thank_you")
32
+
33
+ # List occasions
34
+ sub.add_parser("occasions", help="List all occasions")
35
+ sub.add_parser("budgets", help="List all budget tiers")
36
+
37
+ args = parser.parse_args(argv)
38
+
39
+ if args.command == "search":
40
+ from .client import GiftFinder
41
+ from .models import Occasion, Budget
42
+ finder = GiftFinder(enable_web=not args.no_web)
43
+ occasion = Occasion(args.occasion) if args.occasion else None
44
+ budget = Budget(args.budget) if args.budget else None
45
+ ideas = finder.find(occasion=occasion, budget=budget, query=args.query)
46
+ if args.json:
47
+ print(json.dumps([i.to_dict() for i in ideas], indent=2))
48
+ else:
49
+ for i in ideas:
50
+ print(f" [{i.budget.value:>8}] {i.name}")
51
+ print(f" {i.price_range} — {i.description[:60]}")
52
+ if i.personalization_tip:
53
+ print(f" Tip: {i.personalization_tip}")
54
+ print()
55
+
56
+ elif args.command == "recommend":
57
+ from .client import GiftFinder
58
+ from .models import CustomerProfile, Occasion
59
+ customer = CustomerProfile(
60
+ name=args.name, company=args.company,
61
+ deal_value=args.deal_value, interests=args.interests)
62
+ finder = GiftFinder()
63
+ print(finder.summary_for(customer))
64
+
65
+ elif args.command == "occasions":
66
+ for o in _occasions():
67
+ print(f" {o.value}")
68
+
69
+ elif args.command == "budgets":
70
+ for b in _budgets():
71
+ low, high = b.range
72
+ print(f" {b.value:>8} ${low}-${high}")
73
+
74
+ else:
75
+ parser.print_help()
76
+
77
+
78
+ def _occasions():
79
+ from .models import Occasion
80
+ return list(Occasion)
81
+
82
+ def _budgets():
83
+ from .models import Budget
84
+ return list(Budget)
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -0,0 +1,148 @@
1
+ """GiftFinder — main client that combines catalog search with web search."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass
7
+
8
+ import httpx
9
+
10
+ from .models import GiftIdea, Occasion, Budget, CustomerProfile
11
+ from .catalog import search_catalog, CATALOG
12
+
13
+
14
+ @dataclass
15
+ class GiftFinder:
16
+ """Search for CRM gift ideas from built-in catalog and the web.
17
+
18
+ Parameters
19
+ ----------
20
+ enable_web : bool
21
+ Whether to also search the web for ideas (default True).
22
+ """
23
+
24
+ enable_web: bool = True
25
+
26
+ def find(self, *, occasion: Occasion | None = None, budget: Budget | None = None,
27
+ query: str = "", customer: CustomerProfile | None = None) -> list[GiftIdea]:
28
+ """Find gift ideas matching criteria.
29
+
30
+ Searches the built-in catalog and optionally the web.
31
+ """
32
+ interests = customer.interests if customer else None
33
+
34
+ # Catalog results
35
+ results = search_catalog(occasion=occasion, budget=budget,
36
+ query=query, interests=interests)
37
+
38
+ # Boost score for customer tier match
39
+ if customer and budget is None:
40
+ tier_budget = {
41
+ "enterprise": Budget.VIP,
42
+ "business": Budget.PREMIUM,
43
+ "professional": Budget.STANDARD,
44
+ "starter": Budget.MODEST,
45
+ }
46
+ preferred = tier_budget.get(customer.tier, Budget.STANDARD)
47
+ for g in results:
48
+ if g.budget == preferred:
49
+ g.score += 0.3
50
+
51
+ # Web search (if enabled)
52
+ if self.enable_web and (query or (customer and customer.interests)):
53
+ web_results = asyncio.run(self._web_search(
54
+ query=query, occasion=occasion, budget=budget, customer=customer))
55
+ results.extend(web_results)
56
+
57
+ # Sort by score descending
58
+ results.sort(key=lambda g: g.score, reverse=True)
59
+ return results
60
+
61
+ def recommend(self, customer: CustomerProfile, occasion: Occasion) -> list[GiftIdea]:
62
+ """Smart recommendations based on customer profile and occasion."""
63
+ # Determine budget from customer tier
64
+ tier_budget = {
65
+ "enterprise": Budget.VIP,
66
+ "business": Budget.PREMIUM,
67
+ "professional": Budget.STANDARD,
68
+ "starter": Budget.MODEST,
69
+ }
70
+ budget = tier_budget.get(customer.tier, Budget.STANDARD)
71
+
72
+ ideas = self.find(occasion=occasion, budget=budget, customer=customer)
73
+
74
+ # Further personalize tips
75
+ for idea in ideas:
76
+ if customer.name and idea.personalization_tip:
77
+ idea.personalization_tip = (
78
+ f"For {customer.name}: {idea.personalization_tip}"
79
+ )
80
+
81
+ return ideas[:5]
82
+
83
+ async def _web_search(self, *, query: str, occasion: Occasion | None,
84
+ budget: Budget | None, customer: CustomerProfile | None) -> list[GiftIdea]:
85
+ """Search the web for gift ideas (uses DuckDuckGo instant answers)."""
86
+ search_terms = []
87
+ if query:
88
+ search_terms.append(query)
89
+ if occasion:
90
+ search_terms.append(f"{occasion.value} gift")
91
+ if budget:
92
+ low, high = budget.range
93
+ search_terms.append(f"${low}-${high}")
94
+ if customer and customer.interests:
95
+ search_terms.extend(customer.interests[:2])
96
+ search_terms.append("corporate gift idea")
97
+
98
+ search_query = " ".join(search_terms)
99
+
100
+ async with httpx.AsyncClient() as client:
101
+ try:
102
+ resp = await client.get(
103
+ "https://api.duckduckgo.com/",
104
+ params={"q": search_query, "format": "json", "no_html": 1},
105
+ timeout=10,
106
+ )
107
+ resp.raise_for_status()
108
+ data = resp.json()
109
+
110
+ results = []
111
+ for topic in data.get("RelatedTopics", [])[:5]:
112
+ text = topic.get("Text", "")
113
+ url = topic.get("FirstURL", "")
114
+ if text:
115
+ results.append(GiftIdea(
116
+ name=text[:60],
117
+ description=text,
118
+ price_range="varies",
119
+ budget=budget or Budget.STANDARD,
120
+ occasion=occasion or Occasion.CUSTOM,
121
+ source_url=url,
122
+ category="web-suggestion",
123
+ score=0.3,
124
+ ))
125
+ return results
126
+ except Exception:
127
+ return []
128
+
129
+ def summary_for(self, customer: CustomerProfile) -> str:
130
+ """Print a customer gift strategy summary."""
131
+ lines = [
132
+ f"Gift Strategy for {customer.name}",
133
+ f" Company: {customer.company} ({customer.industry})",
134
+ f" Tier: {customer.tier} (deal value: ${customer.deal_value:,.0f})",
135
+ f" Interests: {', '.join(customer.interests) or 'unknown'}",
136
+ f" Relationship: {customer.relationship_length_months} months",
137
+ "",
138
+ ]
139
+
140
+ for occasion in [Occasion.RENEWAL, Occasion.HOLIDAY, Occasion.THANK_YOU]:
141
+ recs = self.recommend(customer, occasion)
142
+ if recs:
143
+ lines.append(f" [{occasion.value.upper()}]")
144
+ for r in recs[:2]:
145
+ lines.append(f" • {r.name} ({r.price_range}) — {r.personalization_tip}")
146
+ lines.append("")
147
+
148
+ return "\n".join(lines)
@@ -0,0 +1,91 @@
1
+ """Core models for customer profiles, occasions, and gift ideas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class Occasion(str, Enum):
11
+ ONBOARDING = "onboarding"
12
+ RENEWAL = "renewal"
13
+ BIRTHDAY = "birthday"
14
+ HOLIDAY = "holiday"
15
+ MILESTONE = "milestone"
16
+ THANK_YOU = "thank_you"
17
+ APOLOGY = "apology"
18
+ REFERRAL = "referral"
19
+ ANNIVERSARY = "anniversary"
20
+ CLOSING_DEAL = "closing_deal"
21
+ PROMOTION = "promotion"
22
+ CUSTOM = "custom"
23
+
24
+
25
+ class Budget(str, Enum):
26
+ TOKEN = "token" # $0-15
27
+ MODEST = "modest" # $15-50
28
+ STANDARD = "standard" # $50-150
29
+ PREMIUM = "premium" # $150-500
30
+ VIP = "vip" # $500+
31
+
32
+ @property
33
+ def range(self) -> tuple[int, int]:
34
+ return {
35
+ "token": (0, 15),
36
+ "modest": (15, 50),
37
+ "standard": (50, 150),
38
+ "premium": (150, 500),
39
+ "vip": (500, 10000),
40
+ }[self.value]
41
+
42
+
43
+ @dataclass
44
+ class CustomerProfile:
45
+ """Profile of a customer for personalized gift recommendations."""
46
+ name: str
47
+ company: str = ""
48
+ role: str = ""
49
+ industry: str = ""
50
+ interests: list[str] = field(default_factory=list)
51
+ location: str = ""
52
+ relationship_length_months: int = 0
53
+ deal_value: float = 0.0
54
+ notes: str = ""
55
+
56
+ @property
57
+ def tier(self) -> str:
58
+ if self.deal_value >= 100_000:
59
+ return "enterprise"
60
+ elif self.deal_value >= 10_000:
61
+ return "business"
62
+ elif self.deal_value >= 1_000:
63
+ return "professional"
64
+ return "starter"
65
+
66
+
67
+ @dataclass
68
+ class GiftIdea:
69
+ """A single gift recommendation."""
70
+ name: str
71
+ description: str
72
+ price_range: str
73
+ budget: Budget
74
+ occasion: Occasion
75
+ source_url: str = ""
76
+ category: str = ""
77
+ personalization_tip: str = ""
78
+ score: float = 0.0 # relevance score 0-1
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ return {
82
+ "name": self.name,
83
+ "description": self.description,
84
+ "price_range": self.price_range,
85
+ "budget": self.budget.value,
86
+ "occasion": self.occasion.value,
87
+ "source_url": self.source_url,
88
+ "category": self.category,
89
+ "personalization_tip": self.personalization_tip,
90
+ "score": self.score,
91
+ }