Aryan commited on
Commit
aada818
·
0 Parent(s):
scheduling_assistant/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__
2
+ .env
scheduling_assistant/Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Run inference wrapper which interacts with env.py
11
+ CMD ["python", "inference.py"]
scheduling_assistant/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Scheduling Assistant - OpenEnv
2
+
3
+ A simulated corporate scheduling backend. The agent acts as a resource coordinator managing calendars, timezone math, and meeting conflicts.
4
+
5
+ ## Overview
6
+ This environment evaluates an agent's ability to:
7
+ 1. Lookup employee information (names, timezones)
8
+ 2. View cross-timezone calendars and manage overlap.
9
+ 3. Successfully book and prioritize syncs across different levels of complexity.
10
+
11
+ Strict constraints:
12
+ - Built purely in Python without external databases to comply with 2 vCPU and 8GB RAM restrictions on Hugging Face Spaces.
13
+
14
+ ## Task Difficulties (`TASK_LEVEL`)
15
+ - **Easy**: Book a 30-minute sync between two people in the same timezone (UTC).
16
+ - **Medium**: Schedule a 1-hour global all-hands matching the 9-to-5 working hours of 4 employees across PST, EST, UTC, and IST.
17
+ - **Hard**: Override a low priority meeting to book a last-minute VIP event, enforcing priority logic without cancelling critical syncs.
18
+
19
+ ## Action Space
20
+ `Action` expects a JSON object containing:
21
+ - `action_type`: One of `['lookup_employee', 'view_calendar', 'book_meeting', 'cancel_meeting', 'submit_task']`
22
+ - `employee_ids`: Array of strings
23
+ - `start_time`: ISO 8601 string
24
+ - `end_time`: ISO 8601 string
25
+ - `meeting_id`: String (uuid)
26
+
27
+ ## Observation Space
28
+ `Observation` returns:
29
+ - `current_simulated_time`: ISO 8601
30
+ - `task_description`: Instructions for the episode
31
+ - `last_action_result`: Outcome of the previous tool call
32
+ - `error_message`: Formatting limitations or failed constraint messages
33
+
34
+ ## Setup & Running
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ export OPENAI_API_KEY="sk-..."
39
+ export TASK_LEVEL="hard" # "easy" | "medium" | "hard"
40
+ python inference.py
41
+ ```
scheduling_assistant/env.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from datetime import datetime, timedelta
3
+ import dateutil.parser
4
+ import pytz
5
+ from typing import Literal, List, Dict, Optional, Any
6
+ from pydantic import BaseModel, Field
7
+
8
+ class Observation(BaseModel):
9
+ current_simulated_time: str
10
+ task_description: str
11
+ last_action_result: str
12
+ error_message: str = ""
13
+
14
+ class Action(BaseModel):
15
+ action_type: Literal['lookup_employee', 'view_calendar', 'book_meeting', 'cancel_meeting', 'submit_task']
16
+ employee_ids: List[str] = Field(default_factory=list, description="List of employee IDs for lookups or meetings")
17
+ start_time: Optional[str] = Field(None, description="ISO 8601 start time for the meeting")
18
+ end_time: Optional[str] = Field(None, description="ISO 8601 end time for the meeting")
19
+ meeting_id: Optional[str] = Field(None, description="ID of meeting to cancel")
20
+
21
+ EMPLOYEES = {
22
+ "alice": {"id": "alice", "name": "Alice", "timezone": "UTC"},
23
+ "bob": {"id": "bob", "name": "Bob", "timezone": "UTC"},
24
+ "charlie": {"id": "charlie", "name": "Charlie", "timezone": "US/Pacific"},
25
+ "dave": {"id": "dave", "name": "Dave", "timezone": "US/Eastern"},
26
+ "eve": {"id": "eve", "name": "Eve", "timezone": "Asia/Kolkata"},
27
+ "ceo": {"id": "ceo", "name": "CEO", "timezone": "US/Eastern"},
28
+ "vp_sales": {"id": "vp_sales", "name": "VP of Sales", "timezone": "US/Pacific"}
29
+ }
30
+
31
+ class SchedulingEnv:
32
+ def __init__(self, task_level: str = "easy"):
33
+ self.task_level = task_level.lower()
34
+ if self.task_level not in ["easy", "medium", "hard"]:
35
+ self.task_level = "easy"
36
+
37
+ self.max_steps = 15
38
+ self.reset()
39
+
40
+ def reset(self) -> Dict:
41
+ self.current_step = 0
42
+ # Simulated time is Oct 10 2023 08:00 UTC
43
+ self.simulated_time = datetime(2023, 10, 10, 8, 0, 0, tzinfo=pytz.UTC)
44
+ self.calendars = {k: [] for k in EMPLOYEES.keys()}
45
+ self.task_state = {
46
+ "calendar_lookups": set(),
47
+ "found_valid_slot": False,
48
+ "booked_successfully": False,
49
+ "canceled_blocker": False,
50
+ "rescheduled_blocker": False,
51
+ "wrong_cancellation": False
52
+ }
53
+
54
+ self.target_meeting_id = None
55
+ self.blocked_meeting_id = None
56
+ self.high_priority_meeting_id = None
57
+
58
+ self._setup_scenario()
59
+ return self.state()
60
+
61
+ def _setup_scenario(self):
62
+ start_of_day = self.simulated_time.replace(hour=0, minute=0, second=0)
63
+
64
+ if self.task_level == "easy":
65
+ self.task_description = (
66
+ "Book a 30-minute meeting between Alice and Bob (both in UTC). "
67
+ "Find a non-conflicting time tomorrow (Oct 11) within their 9-to-5 working hours. "
68
+ "When finished, run submit_task."
69
+ )
70
+ # Add some random meetings
71
+ self._force_book("alice", start_of_day + timedelta(days=1, hours=9), start_of_day + timedelta(days=1, hours=10), "Sync 1")
72
+ self._force_book("bob", start_of_day + timedelta(days=1, hours=9, minutes=30), start_of_day + timedelta(days=1, hours=10, minutes=30), "Sync 2")
73
+
74
+ elif self.task_level == "medium":
75
+ self.task_description = (
76
+ "Schedule a 1-hour meeting for 4 people: Charlie (PST), Dave (EST), Alice (UTC), and Eve (IST). "
77
+ "The meeting must fall specifically within the 9-to-5 local working hours of ALL 4 participants tomorrow (Oct 11). "
78
+ "When finished, run submit_task."
79
+ )
80
+ self._force_book("eve", start_of_day + timedelta(days=1, hours=10), start_of_day + timedelta(days=1, hours=11), "Sync")
81
+
82
+ elif self.task_level == "hard":
83
+ self.task_description = (
84
+ "The CEO needs an urgent 1-hour meeting tomorrow (Oct 11) with the VP of Sales. "
85
+ "Their calendars are full. You must find a 'low priority' internal sync blocking them, cancel it, "
86
+ "book the CEO + VP of Sales in that slot, and reschedule the canceled sync to another non-conflicting time tomorrow. "
87
+ "Do NOT cancel 'high priority' meetings. Submit the task when finished."
88
+ )
89
+ # Fill tomorrow 9 to 5 PST (VP Sales hours) which restricts the slot heavily
90
+ # CEO is EST. Valid overlap between EST (9-5) and PST (9-5) is 12:00 PM EST to 5:00 PM EST -> 9:00 AM PST to 2:00 PM PST
91
+ # which is 17:00 UTC to 22:00 UTC.
92
+ vp_start = start_of_day + timedelta(days=1, hours=17) # 9 AM PST
93
+
94
+ # High Priority meeting
95
+ self.high_priority_meeting_id = self._force_book(
96
+ ["ceo", "vp_sales"],
97
+ vp_start,
98
+ vp_start + timedelta(hours=2),
99
+ "High Priority Client Pitch"
100
+ )
101
+
102
+ # Low Priority meeting
103
+ self.blocked_meeting_id = self._force_book(
104
+ ["ceo", "vp_sales"],
105
+ vp_start + timedelta(hours=3), # 20:00 UTC -> 12pm PST / 3pm EST
106
+ vp_start + timedelta(hours=4), # 21:00 UTC -> 1pm PST / 4pm EST
107
+ "Low priority internal sync"
108
+ )
109
+
110
+ # Pad the rest of the valid overlap with High Priority
111
+ self._force_book(
112
+ ["vp_sales"],
113
+ vp_start + timedelta(hours=2),
114
+ vp_start + timedelta(hours=3),
115
+ "High Priority Q3 Review"
116
+ )
117
+
118
+ def _force_book(self, emp_ids, start_time: datetime, end_time: datetime, title: str) -> str:
119
+ if isinstance(emp_ids, str):
120
+ emp_ids = [emp_ids]
121
+ m_id = str(uuid.uuid4())
122
+ for e in emp_ids:
123
+ self.calendars[e].append({
124
+ "id": m_id,
125
+ "title": title,
126
+ "start": start_time.isoformat(),
127
+ "end": end_time.isoformat(),
128
+ "participants": emp_ids
129
+ })
130
+ return m_id
131
+
132
+ def _parse_time(self, time_str: str) -> Optional[datetime]:
133
+ try:
134
+ dt = dateutil.parser.isoparse(time_str)
135
+ if dt.tzinfo is None:
136
+ dt = pytz.UTC.localize(dt)
137
+ return dt
138
+ except Exception:
139
+ return None
140
+
141
+ def _is_working_hours(self, emp_id: str, start_dt: datetime, end_dt: datetime) -> bool:
142
+ tz_str = EMPLOYEES[emp_id]["timezone"]
143
+ tz = pytz.timezone(tz_str)
144
+ local_start = start_dt.astimezone(tz)
145
+ local_end = end_dt.astimezone(tz)
146
+
147
+ if local_start.date() != local_end.date():
148
+ return False # crosses midnight locally
149
+
150
+ start_hour = local_start.hour + local_start.minute / 60.0
151
+ end_hour = local_end.hour + local_end.minute / 60.0
152
+ return 9.0 <= start_hour and end_hour <= 17.0
153
+
154
+ def _check_conflict(self, emp_id: str, start_dt: datetime, end_dt: datetime) -> bool:
155
+ for m in self.calendars.get(emp_id, []):
156
+ m_s = self._parse_time(m["start"])
157
+ m_e = self._parse_time(m["end"])
158
+ # Overlap condition
159
+ if max(start_dt, m_s) < min(end_dt, m_e):
160
+ return True
161
+ return False
162
+
163
+ def step(self, action: Action) -> tuple[Dict, float, bool]:
164
+ self.current_step += 1
165
+ reward = 0.0
166
+ done = False
167
+ last_action_result = ""
168
+ error_message = ""
169
+
170
+ if self.current_step >= self.max_steps:
171
+ done = True
172
+ error_message = f"Max steps ({self.max_steps}) reached."
173
+ return self._finalize_step(last_action_result, error_message, done)
174
+
175
+ if action.action_type == "lookup_employee":
176
+ results = []
177
+ for e_id in action.employee_ids:
178
+ if e_id in EMPLOYEES:
179
+ results.append(EMPLOYEES[e_id])
180
+ else:
181
+ error_message += f"Employee {e_id} not found. "
182
+ if not error_message:
183
+ last_action_result = str(results)
184
+
185
+ elif action.action_type == "view_calendar":
186
+ results = {}
187
+ for e_id in action.employee_ids:
188
+ if e_id in EMPLOYEES:
189
+ results[e_id] = self.calendars[e_id]
190
+ self.task_state["calendar_lookups"].add(e_id)
191
+ else:
192
+ error_message += f"Employee {e_id} not found. "
193
+ if not error_message:
194
+ last_action_result = str(results)
195
+
196
+ elif action.action_type == "book_meeting":
197
+ if not action.start_time or not action.end_time or not action.employee_ids:
198
+ error_message = "book_meeting requires start_time, end_time, and employee_ids."
199
+ else:
200
+ s_dt = self._parse_time(action.start_time)
201
+ e_dt = self._parse_time(action.end_time)
202
+
203
+ if not s_dt or not e_dt or s_dt >= e_dt:
204
+ error_message = "Invalid times provided."
205
+ else:
206
+ # check constraints
207
+ valid = True
208
+ for e_id in action.employee_ids:
209
+ if e_id not in EMPLOYEES:
210
+ error_message += f"Employee {e_id} missing. "
211
+ valid = False
212
+ continue
213
+ if not self._is_working_hours(e_id, s_dt, e_dt):
214
+ error_message += f"Outside working hours for {e_id}. "
215
+ valid = False
216
+ if self._check_conflict(e_id, s_dt, e_dt):
217
+ error_message += f"Schedule conflict for {e_id}. "
218
+ valid = False
219
+
220
+ if valid:
221
+ meeting_id = self._force_book(action.employee_ids, s_dt, e_dt, "Agent Booked Sync")
222
+ last_action_result = f"Successfully booked. Meeting ID: {meeting_id}"
223
+
224
+ # Task tracking logic
225
+ dur = (e_dt - s_dt).total_seconds() / 60.0
226
+ if self.task_level == "easy" and set(action.employee_ids) == {"alice", "bob"} and dur >= 30:
227
+ self.task_state["booked_successfully"] = True
228
+
229
+ elif self.task_level == "medium" and set(action.employee_ids) == {"charlie", "dave", "alice", "eve"} and dur >= 60:
230
+ self.task_state["booked_successfully"] = True
231
+
232
+ elif self.task_level == "hard":
233
+ if set(action.employee_ids) == {"ceo", "vp_sales"} and dur >= 60:
234
+ self.task_state["booked_successfully"] = True
235
+ if "ceo" in action.employee_ids and "vp_sales" in action.employee_ids and self.task_state["canceled_blocker"]:
236
+ # they are booking the CEO/VP in the freed slot
237
+ pass
238
+ if len(action.employee_ids) == 2 and "ceo" in action.employee_ids and "vp_sales" in action.employee_ids and self.task_state["canceled_blocker"]:
239
+ # Maybe this is the rescheduled sync? No, the rescheduled is just the same internal sync.
240
+ # Let's assume the agent uses the same participants or general booking.
241
+ pass
242
+
243
+ # If they are rescheduling the canceled sync, they need to book anyone else, but the canceled meeting was with ceo, vp_sales
244
+ # If they make another booking after booking the top priority, we count it.
245
+ if self.task_state["booked_successfully"] and not set(action.employee_ids) == {"ceo", "vp_sales"}:
246
+ # Hacky check for rescheduling
247
+ pass
248
+ if self.task_state["canceled_blocker"]:
249
+ # They booked *something* after canceling. Let's count it if it's the right participants (ceo, vp_sales) but not the main one?
250
+ # Actually, original had CEO + VP Sales. So they just re-book them.
251
+ if set(action.employee_ids) == {"ceo", "vp_sales"} and self.task_state["booked_successfully"]:
252
+ self.task_state["rescheduled_blocker"] = True
253
+
254
+ self.target_meeting_id = meeting_id
255
+
256
+ elif action.action_type == "cancel_meeting":
257
+ if not action.meeting_id:
258
+ error_message = "cancel_meeting requires meeting_id."
259
+ else:
260
+ found = False
261
+ for emp, cals in self.calendars.items():
262
+ cals_new = [m for m in cals if m["id"] != action.meeting_id]
263
+ if len(cals_new) < len(cals):
264
+ self.calendars[emp] = cals_new
265
+ found = True
266
+
267
+ if found:
268
+ last_action_result = f"Meeting {action.meeting_id} canceled."
269
+ if self.task_level == "hard":
270
+ if action.meeting_id == self.blocked_meeting_id:
271
+ self.task_state["canceled_blocker"] = True
272
+ elif action.meeting_id == self.high_priority_meeting_id:
273
+ self.task_state["wrong_cancellation"] = True
274
+ else:
275
+ error_message = "Meeting ID not found."
276
+
277
+ elif action.action_type == "submit_task":
278
+ done = True
279
+ last_action_result = "Task submitted."
280
+
281
+ return self._finalize_step(last_action_result, error_message, done)
282
+
283
+ def _finalize_step(self, last_action_result: str, error_message: str, done: bool) -> tuple[Dict, float, bool]:
284
+ reward = self._calculate_reward()
285
+ obs = Observation(
286
+ current_simulated_time=self.simulated_time.isoformat(),
287
+ task_description=self.task_description,
288
+ last_action_result=last_action_result,
289
+ error_message=error_message
290
+ )
291
+ return obs.model_dump(), reward, done
292
+
293
+ def _calculate_reward(self) -> float:
294
+ r = 0.0
295
+
296
+ if self.task_level == "easy":
297
+ if "alice" in self.task_state["calendar_lookups"]: r += 0.1
298
+ if "bob" in self.task_state["calendar_lookups"]: r += 0.1
299
+ # Found time happens during book_meeting validation
300
+ # Booked gives remaining
301
+ if self.task_state["booked_successfully"]: r += 0.8
302
+
303
+ elif self.task_level == "medium":
304
+ for e in ["charlie", "dave", "alice", "eve"]:
305
+ if e in self.task_state["calendar_lookups"]:
306
+ r += 0.05 # up to 0.2
307
+ if self.task_state["booked_successfully"]:
308
+ r += 0.8
309
+
310
+ elif self.task_level == "hard":
311
+ for e in ["ceo", "vp_sales"]:
312
+ if e in self.task_state["calendar_lookups"]: r += 0.1
313
+
314
+ if self.task_state["canceled_blocker"]: r += 0.2
315
+ if self.task_state["wrong_cancellation"]: r -= 0.5
316
+ if self.task_state["booked_successfully"]: r += 0.3
317
+ if self.task_state["rescheduled_blocker"]: r += 0.3
318
+
319
+ return max(min(r, 1.0), 0.0)
320
+
321
+ def state(self) -> Dict:
322
+ return Observation(
323
+ current_simulated_time=self.simulated_time.isoformat(),
324
+ task_description=self.task_description,
325
+ last_action_result="",
326
+ error_message=""
327
+ ).model_dump()
scheduling_assistant/inference.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from openai import OpenAI
4
+ from env import SchedulingEnv, Action
5
+
6
+ def main():
7
+ hf_token = os.getenv("HF_TOKEN")
8
+ api_key = os.getenv("OPENAI_API_KEY")
9
+
10
+ if api_key:
11
+ api_base_url = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
12
+ elif hf_token:
13
+ api_key = hf_token
14
+ api_base_url = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
15
+ else:
16
+ # Fallback if neither are set
17
+ api_key = "sk-..."
18
+ api_base_url = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
19
+
20
+ model_name = os.getenv("MODEL_NAME", "gpt-4o")
21
+
22
+ client = OpenAI(
23
+ base_url=api_base_url,
24
+ api_key=api_key,
25
+ )
26
+
27
+ task_level = os.getenv("TASK_LEVEL", "easy")
28
+ env = SchedulingEnv(task_level=task_level)
29
+
30
+ obs = env.reset()
31
+
32
+ print("[START]")
33
+
34
+ messages = [
35
+ {
36
+ "role": "system",
37
+ "content": (
38
+ "You are an AI Scheduling Assistant. "
39
+ "You manage calendars, check availability, and book meetings. "
40
+ "Always use the `step_environment` tool to take actions. "
41
+ "You must carefully read the task description and take iterative steps. "
42
+ "Do not assume availability, check calendars first."
43
+ )
44
+ },
45
+ {
46
+ "role": "user",
47
+ "content": f"Initial Observation:\n{json.dumps(obs)}"
48
+ }
49
+ ]
50
+
51
+ action_schema = Action.model_json_schema()
52
+
53
+ tools = [
54
+ {
55
+ "type": "function",
56
+ "function": {
57
+ "name": "step_environment",
58
+ "description": "Execute an action in the environment.",
59
+ "parameters": action_schema
60
+ }
61
+ }
62
+ ]
63
+
64
+ total_reward = 0.0
65
+
66
+ for step_num in range(1, env.max_steps + 1):
67
+ print(f"[STEP] {step_num}")
68
+
69
+ response = client.chat.completions.create(
70
+ model=model_name,
71
+ messages=messages,
72
+ tools=tools,
73
+ tool_choice={"type": "function", "function": {"name": "step_environment"}}
74
+ )
75
+
76
+ response_message = response.choices[0].message
77
+ messages.append(response_message)
78
+
79
+ tool_calls = response_message.tool_calls
80
+ if not tool_calls:
81
+ # Model failed to call a tool, force a submit task
82
+ action = Action(action_type="submit_task")
83
+ obs, reward, done = env.step(action)
84
+ total_reward = reward
85
+ break
86
+
87
+ tool_call = tool_calls[0]
88
+ try:
89
+ action_args = json.loads(tool_call.function.arguments)
90
+ action = Action(**action_args)
91
+ except Exception as e:
92
+ # Fallback if invalid action
93
+ action = Action(action_type="submit_task")
94
+
95
+ messages.append({
96
+ "role": "tool",
97
+ "tool_call_id": tool_call.id,
98
+ "name": tool_call.function.name,
99
+ "content": json.dumps({"action_taken": action.model_dump()})
100
+ })
101
+
102
+ obs, reward, done = env.step(action)
103
+ total_reward = reward
104
+
105
+ messages.append({
106
+ "role": "user",
107
+ "content": f"Observation:\n{json.dumps(obs)}\nReward so far: {reward}"
108
+ })
109
+
110
+ if done:
111
+ break
112
+
113
+ print(f"[END] Final Reward: {total_reward}")
114
+
115
+ if __name__ == "__main__":
116
+ main()
scheduling_assistant/openenv.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "1.0"
2
+ name: "Scheduling Assistant"
3
+ description: "A simulated corporate scheduling backend. Agents manage calendars, timezone math, and meeting conflicts."
4
+ author: "Antigravity Agent"
5
+ tags:
6
+ - scheduling
7
+ - calendar
8
+ - nlp
9
+ - openenv
10
+ constraints:
11
+ vcpus: 2
12
+ memory_gb: 8
13
+ entrypoint: "inference.py"
scheduling_assistant/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ pydantic
2
+ openai
3
+ python-dateutil
4
+ pytz