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.
- restrosphere-0.1.0/PKG-INFO +80 -0
- restrosphere-0.1.0/README.md +53 -0
- restrosphere-0.1.0/agent/__init__.py +1 -0
- restrosphere-0.1.0/agent/auth.py +35 -0
- restrosphere-0.1.0/agent/brain.py +278 -0
- restrosphere-0.1.0/agent/config.py +101 -0
- restrosphere-0.1.0/agent/connectors/__init__.py +0 -0
- restrosphere-0.1.0/agent/connectors/browser.py +87 -0
- restrosphere-0.1.0/agent/connectors/connector_registry.py +62 -0
- restrosphere-0.1.0/agent/connectors/hiring.py +25 -0
- restrosphere-0.1.0/agent/connectors/pos.py +58 -0
- restrosphere-0.1.0/agent/connectors/reservations.py +56 -0
- restrosphere-0.1.0/agent/connectors/reviews.py +30 -0
- restrosphere-0.1.0/agent/connectors/scheduling.py +60 -0
- restrosphere-0.1.0/agent/connectors/scrapers/__init__.py +1 -0
- restrosphere-0.1.0/agent/connectors/scrapers/ameego.py +29 -0
- restrosphere-0.1.0/agent/connectors/scrapers/opentable.py +28 -0
- restrosphere-0.1.0/agent/connectors/scrapers/toast.py +27 -0
- restrosphere-0.1.0/agent/connectors/scrapers/trendnet.py +84 -0
- restrosphere-0.1.0/agent/main.py +850 -0
- restrosphere-0.1.0/agent/memory.py +292 -0
- restrosphere-0.1.0/agent/scheduler.py +110 -0
- restrosphere-0.1.0/agent/skills/__init__.py +0 -0
- restrosphere-0.1.0/agent/skills/base.py +46 -0
- restrosphere-0.1.0/agent/skills/daily_briefing.py +23 -0
- restrosphere-0.1.0/agent/skills/error_cost.py +37 -0
- restrosphere-0.1.0/agent/skills/hiring_score.py +42 -0
- restrosphere-0.1.0/agent/skills/inventory_accuracy.py +51 -0
- restrosphere-0.1.0/agent/skills/labor_efficiency.py +40 -0
- restrosphere-0.1.0/agent/skills/labor_forecast.py +38 -0
- restrosphere-0.1.0/agent/skills/labor_vs_sales.py +61 -0
- restrosphere-0.1.0/agent/skills/shift_performance_score.py +48 -0
- restrosphere-0.1.0/agent/skills/staff_performance.py +134 -0
- restrosphere-0.1.0/agent/skills/training_impact.py +44 -0
- restrosphere-0.1.0/agent/skills/wasted_hours.py +53 -0
- restrosphere-0.1.0/agent/sms.py +71 -0
- restrosphere-0.1.0/agent/stripe_gate.py +45 -0
- restrosphere-0.1.0/pyproject.toml +46 -0
- restrosphere-0.1.0/restrosphere.egg-info/PKG-INFO +80 -0
- restrosphere-0.1.0/restrosphere.egg-info/SOURCES.txt +43 -0
- restrosphere-0.1.0/restrosphere.egg-info/dependency_links.txt +1 -0
- restrosphere-0.1.0/restrosphere.egg-info/entry_points.txt +2 -0
- restrosphere-0.1.0/restrosphere.egg-info/requires.txt +16 -0
- restrosphere-0.1.0/restrosphere.egg-info/top_level.txt +1 -0
- 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)
|