restrosphere 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.
Files changed (45) hide show
  1. restrosphere-0.1.0/PKG-INFO +80 -0
  2. restrosphere-0.1.0/README.md +53 -0
  3. restrosphere-0.1.0/agent/__init__.py +1 -0
  4. restrosphere-0.1.0/agent/auth.py +35 -0
  5. restrosphere-0.1.0/agent/brain.py +278 -0
  6. restrosphere-0.1.0/agent/config.py +101 -0
  7. restrosphere-0.1.0/agent/connectors/__init__.py +0 -0
  8. restrosphere-0.1.0/agent/connectors/browser.py +87 -0
  9. restrosphere-0.1.0/agent/connectors/connector_registry.py +62 -0
  10. restrosphere-0.1.0/agent/connectors/hiring.py +25 -0
  11. restrosphere-0.1.0/agent/connectors/pos.py +58 -0
  12. restrosphere-0.1.0/agent/connectors/reservations.py +56 -0
  13. restrosphere-0.1.0/agent/connectors/reviews.py +30 -0
  14. restrosphere-0.1.0/agent/connectors/scheduling.py +60 -0
  15. restrosphere-0.1.0/agent/connectors/scrapers/__init__.py +1 -0
  16. restrosphere-0.1.0/agent/connectors/scrapers/ameego.py +29 -0
  17. restrosphere-0.1.0/agent/connectors/scrapers/opentable.py +28 -0
  18. restrosphere-0.1.0/agent/connectors/scrapers/toast.py +27 -0
  19. restrosphere-0.1.0/agent/connectors/scrapers/trendnet.py +84 -0
  20. restrosphere-0.1.0/agent/main.py +850 -0
  21. restrosphere-0.1.0/agent/memory.py +292 -0
  22. restrosphere-0.1.0/agent/scheduler.py +110 -0
  23. restrosphere-0.1.0/agent/skills/__init__.py +0 -0
  24. restrosphere-0.1.0/agent/skills/base.py +46 -0
  25. restrosphere-0.1.0/agent/skills/daily_briefing.py +23 -0
  26. restrosphere-0.1.0/agent/skills/error_cost.py +37 -0
  27. restrosphere-0.1.0/agent/skills/hiring_score.py +42 -0
  28. restrosphere-0.1.0/agent/skills/inventory_accuracy.py +51 -0
  29. restrosphere-0.1.0/agent/skills/labor_efficiency.py +40 -0
  30. restrosphere-0.1.0/agent/skills/labor_forecast.py +38 -0
  31. restrosphere-0.1.0/agent/skills/labor_vs_sales.py +61 -0
  32. restrosphere-0.1.0/agent/skills/shift_performance_score.py +48 -0
  33. restrosphere-0.1.0/agent/skills/staff_performance.py +134 -0
  34. restrosphere-0.1.0/agent/skills/training_impact.py +44 -0
  35. restrosphere-0.1.0/agent/skills/wasted_hours.py +53 -0
  36. restrosphere-0.1.0/agent/sms.py +71 -0
  37. restrosphere-0.1.0/agent/stripe_gate.py +45 -0
  38. restrosphere-0.1.0/pyproject.toml +46 -0
  39. restrosphere-0.1.0/restrosphere.egg-info/PKG-INFO +80 -0
  40. restrosphere-0.1.0/restrosphere.egg-info/SOURCES.txt +43 -0
  41. restrosphere-0.1.0/restrosphere.egg-info/dependency_links.txt +1 -0
  42. restrosphere-0.1.0/restrosphere.egg-info/entry_points.txt +2 -0
  43. restrosphere-0.1.0/restrosphere.egg-info/requires.txt +16 -0
  44. restrosphere-0.1.0/restrosphere.egg-info/top_level.txt +1 -0
  45. restrosphere-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: restrosphere
3
+ Version: 0.1.0
4
+ Summary: RestroSphere — The AI that runs your restaurant.
5
+ Author-email: RestroSphere Team <hello@restrosphere.ai>
6
+ Project-URL: Homepage, https://restrosphere.ai
7
+ Project-URL: Documentation, https://docs.restrosphere.ai
8
+ Keywords: restaurant,ai,manager,automation,pos
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: fastapi>=0.104.0
12
+ Requires-Dist: uvicorn[standard]>=0.24.0
13
+ Requires-Dist: anthropic>=0.39.0
14
+ Requires-Dist: click>=8.0.0
15
+ Requires-Dist: rich>=13.0.0
16
+ Requires-Dist: apscheduler>=3.10.0
17
+ Requires-Dist: twilio>=8.10.0
18
+ Requires-Dist: stripe>=7.0.0
19
+ Requires-Dist: python-dotenv>=1.0.0
20
+ Requires-Dist: pydantic>=2.5.0
21
+ Requires-Dist: pydantic-settings>=2.1.0
22
+ Requires-Dist: aiosqlite>=0.19.0
23
+ Requires-Dist: httpx>=0.25.0
24
+ Requires-Dist: psutil>=5.9.0
25
+ Requires-Dist: playwright>=1.40.0
26
+ Requires-Dist: cryptography>=41.0.0
27
+
28
+ # 🍽️ RestroSphere
29
+ ## The AI that runs your restaurant.
30
+
31
+ Monitors your floor. Tracks your staff. Scores your applicants.
32
+ Sends you alerts. Never sleeps.
33
+ All from the dashboard or any chat app you already use.
34
+
35
+ ## Quick Start
36
+ ### Windows
37
+ ```powershell
38
+ iwr -useb https://restrosphere.ai/install.ps1 | iex
39
+ ```
40
+
41
+ ### macOS / Linux
42
+ ```bash
43
+ curl -fsSL https://restrosphere.ai/install.sh | bash
44
+ ```
45
+
46
+ Works on macOS, Windows & Linux. Installs Python and everything else automatically.
47
+
48
+ ## What It Does
49
+ - **Runs 24/7** on your machine as a background service
50
+ - **Connects** to your POS, scheduling, and reservation systems
51
+ - **Monitors** labor vs sales in real time
52
+ - **Scores** job applicants automatically
53
+ - **Tracks** staff performance: tips, upsells, table turns
54
+ - **Sends** proactive alerts when something needs attention
55
+ - **Answers** any question about your restaurant via chat
56
+
57
+ ## Commands
58
+ | Command | Description |
59
+ | :--- | :--- |
60
+ | `restrosphere init` | Set up your restaurant (Step-by-step) |
61
+ | `restrosphere run` | Start the agent (Interactive mode) |
62
+ | `restrosphere daemon start` | Run in background (24/7 Service) |
63
+ | `restrosphere daemon stop` | Stop background agent |
64
+ | `restrosphere daemon status` | Check if service is running |
65
+ | `restrosphere status` | See health of connected systems |
66
+ | `restrosphere skills list` | See active AI capabilities |
67
+
68
+ ## What Gets Connected
69
+ - **POS**: Toast, Square, Clover
70
+ - **Scheduling**: 7shifts, HotSchedules, Deputy
71
+ - **Reservations**: OpenTable, Resy
72
+ - **Reviews**: Google, Yelp
73
+ - **Hiring**: Indeed webhooks
74
+
75
+ ## Cost
76
+ Agent only calls AI when you message it or a scheduled job fires. Never runs in a loop.
77
+ Cost per restaurant: **~$8/month** in API fees.
78
+
79
+ ---
80
+ © 2026 RestroSphere AI. Built with ❤️ for the restaurant industry.
@@ -0,0 +1,53 @@
1
+ # 🍽️ RestroSphere
2
+ ## The AI that runs your restaurant.
3
+
4
+ Monitors your floor. Tracks your staff. Scores your applicants.
5
+ Sends you alerts. Never sleeps.
6
+ All from the dashboard or any chat app you already use.
7
+
8
+ ## Quick Start
9
+ ### Windows
10
+ ```powershell
11
+ iwr -useb https://restrosphere.ai/install.ps1 | iex
12
+ ```
13
+
14
+ ### macOS / Linux
15
+ ```bash
16
+ curl -fsSL https://restrosphere.ai/install.sh | bash
17
+ ```
18
+
19
+ Works on macOS, Windows & Linux. Installs Python and everything else automatically.
20
+
21
+ ## What It Does
22
+ - **Runs 24/7** on your machine as a background service
23
+ - **Connects** to your POS, scheduling, and reservation systems
24
+ - **Monitors** labor vs sales in real time
25
+ - **Scores** job applicants automatically
26
+ - **Tracks** staff performance: tips, upsells, table turns
27
+ - **Sends** proactive alerts when something needs attention
28
+ - **Answers** any question about your restaurant via chat
29
+
30
+ ## Commands
31
+ | Command | Description |
32
+ | :--- | :--- |
33
+ | `restrosphere init` | Set up your restaurant (Step-by-step) |
34
+ | `restrosphere run` | Start the agent (Interactive mode) |
35
+ | `restrosphere daemon start` | Run in background (24/7 Service) |
36
+ | `restrosphere daemon stop` | Stop background agent |
37
+ | `restrosphere daemon status` | Check if service is running |
38
+ | `restrosphere status` | See health of connected systems |
39
+ | `restrosphere skills list` | See active AI capabilities |
40
+
41
+ ## What Gets Connected
42
+ - **POS**: Toast, Square, Clover
43
+ - **Scheduling**: 7shifts, HotSchedules, Deputy
44
+ - **Reservations**: OpenTable, Resy
45
+ - **Reviews**: Google, Yelp
46
+ - **Hiring**: Indeed webhooks
47
+
48
+ ## Cost
49
+ Agent only calls AI when you message it or a scheduled job fires. Never runs in a loop.
50
+ Cost per restaurant: **~$8/month** in API fees.
51
+
52
+ ---
53
+ © 2026 RestroSphere AI. Built with ❤️ for the restaurant industry.
@@ -0,0 +1 @@
1
+ # RestroSphere AI Agent
@@ -0,0 +1,35 @@
1
+ import os
2
+ import base64
3
+ from pathlib import Path
4
+ from cryptography.fernet import Fernet
5
+ from cryptography.hazmat.primitives import hashes
6
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
7
+
8
+ def _get_key():
9
+ """Get or create a master encryption key."""
10
+ key = os.getenv("MASTER_KEY")
11
+
12
+ if not key:
13
+ # Generate a random key
14
+ key = Fernet.generate_key().decode()
15
+ # Save to global .env via config logic
16
+ from agent.main import _write_env
17
+ _write_env("MASTER_KEY", key)
18
+ os.environ["MASTER_KEY"] = key
19
+
20
+ return key.encode()
21
+
22
+ def encrypt_cred(text: str) -> str:
23
+ """Encrypt a string using the master key."""
24
+ if not text: return ""
25
+ f = Fernet(_get_key())
26
+ return f.encrypt(text.encode()).decode()
27
+
28
+ def decrypt_cred(token: str) -> str:
29
+ """Decrypt a string using the master key."""
30
+ if not token: return ""
31
+ try:
32
+ f = Fernet(_get_key())
33
+ return f.decrypt(token.encode()).decode()
34
+ except Exception:
35
+ return ""
@@ -0,0 +1,278 @@
1
+ """
2
+ RestroSphere Brain — Claude AI integration with streaming
3
+ """
4
+ import json
5
+ from datetime import datetime, date
6
+ from typing import AsyncGenerator
7
+ from agent.config import config
8
+ from agent import memory
9
+
10
+
11
+ def build_system_prompt(context: dict | None = None) -> str:
12
+ """Build the system prompt with full restaurant context."""
13
+ ctx = context or {}
14
+ today = datetime.now().strftime("%A, %B %d, %Y")
15
+ now_time = datetime.now().strftime("%I:%M %p")
16
+
17
+ restaurant = ctx.get("restaurant_name", config.restaurant_name)
18
+ sales = ctx.get("current_sales", 2400)
19
+ covers = ctx.get("cover_count", 14)
20
+ labor = ctx.get("labor_percent", 28.4)
21
+ top_item = ctx.get("top_selling_item", "Grilled Salmon")
22
+ staff = ctx.get("staff", [
23
+ {"name": "Marco", "role": "Head Chef", "status": "on_shift"},
24
+ {"name": "Priya", "role": "Server", "tips": 142, "upsells": 4, "tables": 6, "status": "on_shift"},
25
+ {"name": "Jake", "role": "Server", "tips": 89, "upsells": 1, "tables": 4, "status": "on_shift"},
26
+ ])
27
+ reservations = ctx.get("reservations", [
28
+ {"time": "6:00 PM", "name": "Johnson", "covers": 4},
29
+ {"time": "7:30 PM", "name": "Martinez", "covers": 2},
30
+ {"time": "8:00 PM", "name": "Chen", "covers": 6},
31
+ ])
32
+ alerts = ctx.get("alerts", [
33
+ "Labor trending high — currently 28.4%",
34
+ "3 new applicants for Line Cook position",
35
+ "New 3-star Google review needs response",
36
+ ])
37
+
38
+ manager_notes = ctx.get("manager_notes", [])
39
+ if manager_notes:
40
+ alerts.extend([f"Note: {n}" for n in manager_notes])
41
+
42
+ # New Performance Metrics
43
+ metrics = ctx.get("performance_metrics", {
44
+ "labor_efficiency": "$64.20/hr",
45
+ "wasted_hours": "1.5h ($22.50 cost)",
46
+ "qsa_cost": "$36.00 tonight",
47
+ "labor_forecast": "Actual 28.4% vs Forecast 25.0% (+3.4%)"
48
+ })
49
+
50
+ staff_block = "\n".join(
51
+ f" - {s['name']} ({s['role']}) — "
52
+ + (f"Tips: ${s.get('tips', 0)}, Score: {s.get('score', 85)}/100, QSA Cost: ${s.get('qsa', 0)}" if s['role'] != 'Head Chef' else "Kitchen lead")
53
+ for s in staff
54
+ )
55
+
56
+ res_block = "\n".join(f" - {r['time']}: {r['name']} ({r['covers']} covers)" for r in reservations)
57
+ alert_block = "\n".join(f" - {a}" for a in alerts)
58
+
59
+ return f"""You are a data-driven restaurant ops manager and the digital brain of "{restaurant}".
60
+ You have access to:
61
+ - Full POS integration (Toast/TrendNet): Every ticket, itemized sales, void logs, and hourly revenue velocity.
62
+ - Scheduling data (7shifts/Ameego): Full shift rosters, clock-in/out timestamps, and labor cost vs. performance per role.
63
+ - Reservation structure (OpenTable): Cover counts, guest notes, VIP tags, and floor plan pacing.
64
+ - Operational metrics: QSA error costs, idle hours, inventory discrepancies, and training ROI.
65
+
66
+ TODAY: {today} | TIME: {now_time}
67
+ RESTAURANT: {restaurant}
68
+
69
+ LIVE METRICS:
70
+ Sales today: ${sales:,.0f} | Labor cost: {labor}%
71
+ Labor Efficiency: {metrics['labor_efficiency']}
72
+ Wasted Hours: {metrics['wasted_hours']}
73
+ QSA Error Cost: {metrics['qsa_cost']}
74
+ Forecast Dev: {metrics['labor_forecast']}
75
+ Top seller: {top_item}
76
+
77
+ STAFF PERFORMANCE:
78
+ {staff_block}
79
+
80
+ TONIGHT'S RESERVATIONS:
81
+ {res_block}
82
+
83
+ ACTIVE ALERTS:
84
+ {alert_block}
85
+
86
+ PERSONALITY & RULES:
87
+ - If a manager asks "who is underperforming on FOH?", give a direct, honest answer. Compare their tips, QSA errors, and idle time against the team average.
88
+ - If a manager mentions a staff member, check for any "Manager Notes" or "Historical Observations" and include them (e.g., "Jake was slow on table turns tonight, which matches your note from Tuesday").
89
+ - Be direct. If someone is underperforming, say it clearly with the numbers.
90
+ - If labor % is off, explain why (using forecast dev) and recommend action.
91
+ - Always provide one clear recommendation at the end of every analysis.
92
+ - You can parse commands like "Log note: name observation" to save manager feedback.
93
+ """
94
+
95
+
96
+ async def parse_chat_commands(message: str, manager: str):
97
+ """Detect and execute manual logging commands from chat."""
98
+ msg = message.lower()
99
+ day = date.today().isoformat()
100
+
101
+ # "Log QSA: [name] made [num] [item] errors at $[price]"
102
+ if "log qsa:" in msg:
103
+ import re
104
+ match = re.search(r"log qsa: (\w+) made (\d+) (.+) errors at \$(\d+)", msg)
105
+ if match:
106
+ name, num, item, price = match.groups()
107
+ total = int(num) * float(price)
108
+ await memory.save_staff_event(day, name, "qsa", f"{num}x {item} errors", total)
109
+ return f"✅ Logged QSA error for {name}: {num}x {item} (${total}). Performance score updated."
110
+
111
+ # "Log note: [name] [observation]"
112
+ if "log note:" in msg:
113
+ match = re.search(r"log note: (\w+) (.+)", msg)
114
+ if match:
115
+ name, observation = match.groups()
116
+ await memory.save_staff_event(day, name, "note", observation)
117
+ return f"📝 Saved manager note for {name}: \"{observation}\". This context will be used in future performance reviews."
118
+
119
+ # "Log complaint: [name] had a guest complaint about [reason]"
120
+ if "log complaint:" in msg:
121
+ match = re.search(r"log complaint: (\w+) had a guest complaint about (.+)", msg)
122
+ if match:
123
+ name, reason = match.groups()
124
+ await memory.save_staff_event(day, name, "complaint", reason, -25) # -25 points
125
+ return f"⚠️ Logged guest complaint for {name} regarding {reason}."
126
+
127
+ # "Log duty completed: [name] finished [task]"
128
+ if "log duty completed:" in msg:
129
+ match = re.search(r"log duty completed: (\w+) finished (.+)", msg)
130
+ if match:
131
+ name, task = match.groups()
132
+ await memory.save_staff_event(day, name, "duty", task, 10) # +10 points
133
+ return f"✅ Logged completed duty: {name} finished {task}."
134
+
135
+ # "Mark training: [name] [date]"
136
+ if "mark training:" in msg:
137
+ match = re.search(r"mark training: (\w+) (.+)", msg)
138
+ if match:
139
+ name, start_date = match.groups()
140
+ await memory.save_staff_event(day, name, "training_start", f"Started training on {start_date}")
141
+ return f"🎓 Marked {name} as 'In Training' starting {start_date}. I will track their performance over the next 14 days."
142
+
143
+ return None
144
+
145
+ async def stream_response(
146
+ message: str,
147
+ manager: str = "Manager",
148
+ history: list[dict] | None = None,
149
+ context: dict | None = None,
150
+ ) -> AsyncGenerator[str, None]:
151
+ """Stream Claude response token by token."""
152
+
153
+ # 1. Process manual commands
154
+ cmd_response = await parse_chat_commands(message, manager)
155
+ if cmd_response:
156
+ yield cmd_response
157
+ yield "\n\n[DONE]"
158
+ return
159
+
160
+ # 2. Save user message
161
+ await memory.save_message("user", message, manager)
162
+
163
+ if not config.has_anthropic or config.demo_mode:
164
+ # Realistic Demo Responses for "The Golden Fork"
165
+ msg_lower = message.lower()
166
+
167
+ if "briefing" in msg_lower or "morning" in msg_lower:
168
+ response = (
169
+ "☀️ **Morning Briefing: The Golden Fork**\n\n"
170
+ "Good morning! Here's the outlook for tonight:\n"
171
+ "- **Reservations:** 3 bookings (12 covers total). Peak at 7:30 PM.\n"
172
+ "- **Labor Alert:** Projected labor is at **34%** — this is high. I recommend **cutting Jake at 9:00 PM** if the floor stays manageable.\n"
173
+ "- **Staffing:** 4 members on shift (Marco, Priya, Jake, Sarah).\n"
174
+ "- **Goal:** Focus on upselling the Ribeye specials to hit our $2,500 sales target."
175
+ )
176
+ elif "staff" in msg_lower or "performance" in msg_lower:
177
+ response = (
178
+ "📊 **Staff Performance Stats**\n\n"
179
+ "Current Floor Performance:\n"
180
+ "1. **Priya (Server):** 94/100 score. $142 in tips tonight. 4 upsells.\n"
181
+ "2. **Jake (Server):** 78/100 score. $89 in tips. Slow on ticket times.\n"
182
+ "3. **Sarah (Server):** 88/100 score. $110 in tips. Consistent.\n"
183
+ "4. **Marco (Head Chef):** Kitchen lead. All tickets out < 15 mins.\n\n"
184
+ "*Recommendation: Priya is crushing it on upsells. Jake needs a nudge on table turns.*"
185
+ )
186
+ elif "reservation" in msg_lower:
187
+ response = (
188
+ "📅 **Tonight's Reservations**\n\n"
189
+ "- **6:00 PM:** Johnson (Party of 4)\n"
190
+ "- **7:30 PM:** Martinez (Party of 2) — *Window table requested*\n"
191
+ "- **8:00 PM:** Chen (Party of 6)\n\n"
192
+ "All slots are confirmed in OpenTable."
193
+ )
194
+ elif "labor" in msg_lower:
195
+ response = (
196
+ "🚨 **Labor Analysis**\n\n"
197
+ "Current Labor: **34%** (Target: 28%)\n"
198
+ "Observation: Sales volume is slightly lower than forecast for this hour.\n\n"
199
+ "**Action:** Recommend cutting **Jake** at 9:00 PM to bring us back to 29%."
200
+ )
201
+ elif "underperforming" in msg_lower or "foh" in msg_lower:
202
+ response = (
203
+ "🚨 **Performance Alert: FOH Underperformance**\n\n"
204
+ "Based on tonight's data compared to team averages:\n"
205
+ "- **Jake:** Currently underperforming. Tips ($89) are **23% below average**. He has **3 QSA errors** (wrong drink prep) costing $36.00, and a **25-minute idle gap** after clock-in.\n"
206
+ "- **Historical Context:** You noted earlier this week that 'Jake was slow on table turns tonight, third time this week'. The data confirms this pattern is continuing.\n\n"
207
+ "**Action:** I recommend a one-on-one with Jake regarding service speed and drink specs before the weekend rush."
208
+ )
209
+ elif "note" in msg_lower:
210
+ response = (
211
+ "📝 **Manager Notes Summary**\n\n"
212
+ "Recent observations for staff:\n"
213
+ "- **Jake:** 'Slow on table turns tonight, third time this week' (Saved Tuesday)\n"
214
+ "- **Sarah:** 'Crushed the wine upselling during the 7pm rush' (Saved Wednesday)\n\n"
215
+ "I track these patterns to help you build a complete picture of staff growth."
216
+ )
217
+ else:
218
+ response = (
219
+ "👋 Hey! I'm RestroSphere AI, your restaurant's digital brain.\n\n"
220
+ "I'm currently running in **demo mode** (or no API key found).\n\n"
221
+ "Try asking me about:\n"
222
+ "- 'Who is underperforming on FOH?'\n"
223
+ "- 'Show me manager notes for Jake'\n"
224
+ "- 'How is the morning briefing?'\n"
225
+ "- 'Check labor status'\n\n"
226
+ "To activate me fully with real-time Claude 3.5 Sonnet analysis, add your `ANTHROPIC_API_KEY` to `.env`."
227
+ )
228
+
229
+ await memory.save_message("assistant", response, manager)
230
+ for char in response:
231
+ yield char
232
+ return
233
+
234
+ try:
235
+ from anthropic import Anthropic
236
+
237
+ client = Anthropic(api_key=config.anthropic_api_key)
238
+
239
+ # Build messages from history
240
+ messages = []
241
+ if history:
242
+ for h in history[-10:]:
243
+ messages.append({"role": h["role"], "content": h["content"]})
244
+
245
+ messages.append({"role": "user", "content": message})
246
+
247
+ system_prompt = build_system_prompt(context)
248
+
249
+ full_response = ""
250
+ with client.messages.stream(
251
+ model=config.ai_model,
252
+ max_tokens=800,
253
+ system=system_prompt,
254
+ messages=messages,
255
+ ) as stream:
256
+ for text in stream.text_stream:
257
+ full_response += text
258
+ yield text
259
+
260
+ # Save assistant response
261
+ await memory.save_message("assistant", full_response, manager)
262
+
263
+ except Exception as e:
264
+ error_msg = f"Error connecting to AI: {str(e)}"
265
+ await memory.save_message("assistant", error_msg, manager)
266
+ yield error_msg
267
+
268
+
269
+ async def get_single_response(
270
+ message: str,
271
+ manager: str = "Manager",
272
+ context: dict | None = None,
273
+ ) -> str:
274
+ """Get a complete (non-streaming) response."""
275
+ full = ""
276
+ async for chunk in stream_response(message, manager, context=context):
277
+ full += chunk
278
+ return full
@@ -0,0 +1,101 @@
1
+ """
2
+ RestroSphere Agent Configuration
3
+ Reads from .env file in the global RestroSphere home directory
4
+ """
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from dotenv import load_dotenv
9
+
10
+ def get_home_dir() -> Path:
11
+ """Get the platform-specific home directory for RestroSphere."""
12
+ if sys.platform == "win32":
13
+ path = Path("C:/RestroSphere")
14
+ else:
15
+ path = Path.home() / ".restrosphere"
16
+
17
+ path.mkdir(parents=True, exist_ok=True)
18
+ return path
19
+
20
+ # Constants
21
+ HOME_DIR = get_home_dir()
22
+ ENV_PATH = HOME_DIR / ".env"
23
+
24
+ # Load .env from global home directory
25
+ load_dotenv(ENV_PATH)
26
+
27
+ class Config:
28
+ """All agent configuration — reads from environment."""
29
+
30
+ # Mode
31
+ demo_mode: bool = os.getenv("DEMO_MODE", "false").lower() == "true"
32
+
33
+ # Restaurant
34
+ restaurant_name: str = os.getenv("RESTAURANT_NAME", "The Golden Fork")
35
+ restaurant_address: str = os.getenv("RESTAURANT_ADDRESS", "")
36
+ restaurant_phone: str = os.getenv("RESTAURANT_PHONE", "")
37
+
38
+ # Manager contacts (comma-separated)
39
+ manager_phones: list[str] = [
40
+ p.strip() for p in os.getenv("MANAGER_PHONES", "").split(",") if p.strip()
41
+ ]
42
+ manager_names: list[str] = [
43
+ n.strip() for n in os.getenv("MANAGER_NAMES", "Manager").split(",") if n.strip()
44
+ ]
45
+
46
+ # API Keys
47
+ anthropic_api_key: str = os.getenv("ANTHROPIC_API_KEY", "")
48
+ ai_model: str = os.getenv("AI_MODEL", "claude-3-5-haiku-20241022")
49
+ openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
50
+ twilio_sid: str = os.getenv("TWILIO_ACCOUNT_SID", "")
51
+ twilio_token: str = os.getenv("TWILIO_AUTH_TOKEN", "")
52
+ twilio_phone: str = os.getenv("TWILIO_PHONE_NUMBER", "")
53
+ stripe_secret_key: str = os.getenv("STRIPE_SECRET_KEY", "")
54
+
55
+ # Connectors
56
+ pos_provider: str = os.getenv("POS_PROVIDER", "") # toast, square, clover, trendnet
57
+ pos_api_key: str = os.getenv("POS_API_KEY", "")
58
+ scheduling_provider: str = os.getenv("SCHEDULING_PROVIDER", "")
59
+ scheduling_api_key: str = os.getenv("SCHEDULING_API_KEY", "")
60
+ reservations_provider: str = os.getenv("RESERVATIONS_PROVIDER", "")
61
+ reservations_api_key: str = os.getenv("RESERVATIONS_API_KEY", "")
62
+
63
+ # Convex
64
+ convex_url: str = os.getenv("NEXT_PUBLIC_CONVEX_URL", "")
65
+ convex_deploy_key: str = os.getenv("CONVEX_DEPLOY_KEY", "")
66
+
67
+ # Server
68
+ host: str = os.getenv("AGENT_HOST", "0.0.0.0")
69
+ port: int = int(os.getenv("AGENT_PORT", "8000"))
70
+ debug: bool = os.getenv("AGENT_DEBUG", "false").lower() == "true"
71
+
72
+ # Database & Logs
73
+ db_path: str = os.getenv("AGENT_DB_PATH", str(HOME_DIR / "restrosphere.db"))
74
+ log_path: str = str(HOME_DIR / "agent.log")
75
+
76
+ @property
77
+ def has_anthropic(self) -> bool:
78
+ return bool(self.anthropic_api_key)
79
+
80
+ @property
81
+ def has_twilio(self) -> bool:
82
+ return bool(self.twilio_sid and self.twilio_token and self.twilio_phone)
83
+
84
+ @property
85
+ def has_stripe(self) -> bool:
86
+ return bool(self.stripe_secret_key)
87
+
88
+ @property
89
+ def has_pos(self) -> bool:
90
+ return bool(self.pos_provider and self.pos_api_key) or self.pos_provider == "trendnet"
91
+
92
+ @property
93
+ def has_scheduling(self) -> bool:
94
+ return bool(self.scheduling_provider and self.scheduling_api_key)
95
+
96
+ @property
97
+ def has_reservations(self) -> bool:
98
+ return bool(self.reservations_provider and self.reservations_api_key)
99
+
100
+
101
+ config = Config()
File without changes
@@ -0,0 +1,87 @@
1
+ from playwright.async_api import async_playwright
2
+ import asyncio
3
+ import logging
4
+
5
+ logger = logging.getLogger("restrosphere.browser")
6
+
7
+ class BrowserConnector:
8
+ def __init__(self, system_name, login_url, username, password):
9
+ self.system_name = system_name
10
+ self.login_url = login_url
11
+ self.username = username
12
+ self.password = password
13
+
14
+ async def login(self, page):
15
+ """Generic login — finds username/password fields and submits"""
16
+ logger.info(f"Attempting login to {self.system_name} at {self.login_url}")
17
+ await page.goto(self.login_url)
18
+
19
+ # Try common field selectors for username
20
+ username_selectors = [
21
+ 'input[type="email"]',
22
+ 'input[name="email"]',
23
+ 'input[name="username"]',
24
+ 'input[id*="user"]',
25
+ 'input[placeholder*="Email"]',
26
+ 'input[placeholder*="Username"]'
27
+ ]
28
+
29
+ # Try common field selectors for password
30
+ password_selectors = [
31
+ 'input[type="password"]',
32
+ 'input[name="password"]',
33
+ 'input[id*="pass"]'
34
+ ]
35
+
36
+ # Fill username
37
+ filled_user = False
38
+ for selector in username_selectors:
39
+ try:
40
+ await page.wait_for_selector(selector, timeout=2000)
41
+ await page.fill(selector, self.username)
42
+ filled_user = True
43
+ break
44
+ except: continue
45
+
46
+ if not filled_user:
47
+ raise Exception(f"Could not find username field for {self.system_name}")
48
+
49
+ # Fill password
50
+ filled_pass = False
51
+ for selector in password_selectors:
52
+ try:
53
+ await page.fill(selector, self.password)
54
+ filled_pass = True
55
+ break
56
+ except: continue
57
+
58
+ if not filled_pass:
59
+ raise Exception(f"Could not find password field for {self.system_name}")
60
+
61
+ # Click submit
62
+ submit_selectors = [
63
+ 'button[type="submit"]',
64
+ 'input[type="submit"]',
65
+ 'button:has-text("Login")',
66
+ 'button:has-text("Sign In")'
67
+ ]
68
+
69
+ clicked = False
70
+ for selector in submit_selectors:
71
+ try:
72
+ await page.click(selector, timeout=2000)
73
+ clicked = True
74
+ break
75
+ except: continue
76
+
77
+ if not clicked:
78
+ # Fallback: Enter key
79
+ await page.keyboard.press("Enter")
80
+
81
+ await page.wait_for_load_state('networkidle')
82
+ logger.info(f"Login submitted for {self.system_name}")
83
+
84
+ async def scrape(self, page, scrape_fn):
85
+ """Login then run a scrape function"""
86
+ await self.login(page)
87
+ return await scrape_fn(page)