aion-agent 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.
- aion_agent-0.1.0/LICENSE +21 -0
- aion_agent-0.1.0/PKG-INFO +219 -0
- aion_agent-0.1.0/README.md +189 -0
- aion_agent-0.1.0/aion/__init__.py +3 -0
- aion_agent-0.1.0/aion/__main__.py +5 -0
- aion_agent-0.1.0/aion/asp_model.py +200 -0
- aion_agent-0.1.0/aion/auth.py +131 -0
- aion_agent-0.1.0/aion/cli.py +660 -0
- aion_agent-0.1.0/aion/config.py +118 -0
- aion_agent-0.1.0/aion/date_parser.py +162 -0
- aion_agent-0.1.0/aion/display.py +182 -0
- aion_agent-0.1.0/aion/google_cal.py +236 -0
- aion_agent-0.1.0/aion/intent.py +233 -0
- aion_agent-0.1.0/aion/ollama.py +131 -0
- aion_agent-0.1.0/aion/setup.py +172 -0
- aion_agent-0.1.0/aion/solver.py +123 -0
- aion_agent-0.1.0/aion_agent.egg-info/PKG-INFO +219 -0
- aion_agent-0.1.0/aion_agent.egg-info/SOURCES.txt +25 -0
- aion_agent-0.1.0/aion_agent.egg-info/dependency_links.txt +1 -0
- aion_agent-0.1.0/aion_agent.egg-info/entry_points.txt +2 -0
- aion_agent-0.1.0/aion_agent.egg-info/requires.txt +7 -0
- aion_agent-0.1.0/aion_agent.egg-info/top_level.txt +1 -0
- aion_agent-0.1.0/pyproject.toml +58 -0
- aion_agent-0.1.0/setup.cfg +4 -0
- aion_agent-0.1.0/tests/test_date_parser.py +75 -0
- aion_agent-0.1.0/tests/test_intent.py +177 -0
- aion_agent-0.1.0/tests/test_solver.py +80 -0
aion_agent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sheikh Abdul Munim
|
|
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.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aion-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI calendar scheduling agent for Google Calendar
|
|
5
|
+
Author: Sheikh Abdul Munim
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sheikhmunim/Aion
|
|
8
|
+
Project-URL: Repository, https://github.com/sheikhmunim/Aion
|
|
9
|
+
Project-URL: Issues, https://github.com/sheikhmunim/Aion/issues
|
|
10
|
+
Keywords: calendar,scheduling,google-calendar,cli,agent
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: clingo>=5.6.0
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: rich>=13.0.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Aion
|
|
32
|
+
|
|
33
|
+
**AI-powered calendar agent for Google Calendar.**
|
|
34
|
+
|
|
35
|
+
Schedule, list, reschedule, and find free time — all from natural language in your terminal.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
aion > schedule gym tomorrow morning
|
|
39
|
+
Finding optimal slot for 'gym'...
|
|
40
|
+
Schedule 'gym' on February 19, 2026 at 07:00 for 60 min? [y/n]: y
|
|
41
|
+
✔ Created! 'gym' on 2026-02-19 at 07:00
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Part of [A.U.R.A](https://github.com/sheikhmunim) (Autonomous Unified Reasoning Assistant).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Natural language** — "schedule dentist friday at 2pm for 45 min", "what's on tomorrow?"
|
|
51
|
+
- **Smart scheduling** — ASP/Clingo constraint solver finds optimal slots avoiding conflicts
|
|
52
|
+
- **Google Calendar sync** — reads and writes real events via Calendar API v3
|
|
53
|
+
- **Conflict detection** — warns on overlaps, offers alternatives
|
|
54
|
+
- **User preferences** — block time slots (lunch, sleep), set default morning/afternoon/evening
|
|
55
|
+
- **Timezone-aware** — auto-detects your timezone from Google Calendar on login
|
|
56
|
+
- **Ollama NLU** (optional) — local LLM fallback for complex commands, auto-installs on first run
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Architecture
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
User Input
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌──────────────┐ ┌──────────────┐
|
|
67
|
+
│ Regex NLU │───▶│ Ollama LLM │ (optional fallback)
|
|
68
|
+
│ (intent.py) │ │ (ollama.py) │
|
|
69
|
+
└──────┬───────┘ └──────────────┘
|
|
70
|
+
│
|
|
71
|
+
▼
|
|
72
|
+
┌──────────────┐ ┌──────────────┐
|
|
73
|
+
│ ASP Solver │───▶│ Clingo │ (constraint solving)
|
|
74
|
+
│ (solver.py) │ │ │
|
|
75
|
+
└──────┬───────┘ └──────────────┘
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
┌──────────────┐
|
|
79
|
+
│ Google Cal │ (httpx async)
|
|
80
|
+
│ (google_cal) │
|
|
81
|
+
└──────────────┘
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install aion-agent
|
|
90
|
+
aion login
|
|
91
|
+
aion
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
That's it. `aion login` opens your browser for Google sign-in. Your timezone is auto-detected. No API keys or configuration needed.
|
|
95
|
+
|
|
96
|
+
On first run, Aion offers to install [Ollama](https://ollama.com) for smarter natural language understanding — this is optional.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
|
|
102
|
+
**From PyPI:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install aion-agent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**From source:**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/sheikhmunim/Aion.git
|
|
112
|
+
cd Aion
|
|
113
|
+
pip install -e .
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Requires **Python 3.10+**.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Usage
|
|
121
|
+
|
|
122
|
+
Start the interactive CLI:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
aion
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Commands
|
|
129
|
+
|
|
130
|
+
| Action | Examples |
|
|
131
|
+
|--------|----------|
|
|
132
|
+
| **Schedule** | `schedule gym tomorrow morning`, `add meeting at 3pm for 90 min` |
|
|
133
|
+
| **List** | `what's on today?`, `show my calendar this week`, `what tomorrow?` |
|
|
134
|
+
| **Delete** | `cancel gym tomorrow`, `delete meeting` |
|
|
135
|
+
| **Update** | `move gym to 3pm`, `reschedule meeting to friday` |
|
|
136
|
+
| **Free slots** | `when am I free tomorrow?`, `free slots this week` |
|
|
137
|
+
| **Best time** | `best time for a 2h study session` |
|
|
138
|
+
| **Preferences** | `preferences` — manage blocked times and defaults |
|
|
139
|
+
| **Login/Logout** | `login`, `logout` |
|
|
140
|
+
| **Help** | `help` |
|
|
141
|
+
| **Quit** | `quit` or `exit` |
|
|
142
|
+
|
|
143
|
+
### Preferences
|
|
144
|
+
|
|
145
|
+
Block recurring time slots and set defaults:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
aion > preferences
|
|
149
|
+
┌─────────────────────────────────────────────┐
|
|
150
|
+
│ 1. Add a blocked time slot │
|
|
151
|
+
│ 2. Remove a blocked slot │
|
|
152
|
+
│ 3. Change default time preference │
|
|
153
|
+
│ 4. Back │
|
|
154
|
+
└─────────────────────────────────────────────┘
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Blocked slots (e.g. lunch 12:00-13:00 on weekdays) are respected by the scheduler — it won't suggest times during those windows.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
Config lives at `~/.aion/config.json`. All options can also be set via environment variables with `AION_` prefix.
|
|
164
|
+
|
|
165
|
+
| Key | Env var | Default | Description |
|
|
166
|
+
|-----|---------|---------|-------------|
|
|
167
|
+
| `google_client_id` | `AION_GOOGLE_CLIENT_ID` | Built-in | OAuth client ID (override with your own if needed) |
|
|
168
|
+
| `google_client_secret` | `AION_GOOGLE_CLIENT_SECRET` | Built-in | OAuth client secret |
|
|
169
|
+
| `timezone` | `AION_TIMEZONE` | `UTC` | IANA timezone (auto-detected on login) |
|
|
170
|
+
| `default_duration` | `AION_DEFAULT_DURATION` | `60` | Default event duration in minutes |
|
|
171
|
+
| `ollama_url` | `AION_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
172
|
+
| `ollama_model` | `AION_OLLAMA_MODEL` | `qwen2.5:0.5b` | Ollama model for NLU |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Install with dev dependencies
|
|
180
|
+
pip install -e ".[dev]"
|
|
181
|
+
|
|
182
|
+
# Run tests
|
|
183
|
+
pytest tests/ -v
|
|
184
|
+
|
|
185
|
+
# Lint
|
|
186
|
+
ruff check aion/
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## How it works
|
|
192
|
+
|
|
193
|
+
1. **Intent classification** — Regex patterns match commands (schedule, list, delete, etc.) with confidence scores. Falls back to Ollama LLM for ambiguous input.
|
|
194
|
+
|
|
195
|
+
2. **Date parsing** — Handles "today", "tomorrow", weekday names, "this/next week", specific dates like "March 5th", and common typos.
|
|
196
|
+
|
|
197
|
+
3. **Constraint solving** — The ASP/Clingo solver models the day as 30-minute slots (6AM-10PM), marks busy times from existing events and user preferences, then finds optimal placements with time-of-day preferences.
|
|
198
|
+
|
|
199
|
+
4. **Google Calendar API** — All reads/writes go through Calendar API v3 via httpx async. Token refresh is automatic.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Dependencies
|
|
204
|
+
|
|
205
|
+
| Package | Purpose |
|
|
206
|
+
|---------|---------|
|
|
207
|
+
| [clingo](https://potassco.org/clingo/) | ASP constraint solver |
|
|
208
|
+
| [httpx](https://www.python-httpx.org/) | Async HTTP client (Google Calendar + Ollama) |
|
|
209
|
+
| [rich](https://rich.readthedocs.io/) | Terminal UI |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Privacy
|
|
214
|
+
|
|
215
|
+
Aion runs entirely on your machine. No calendar data is sent to external servers. See [PRIVACY.md](PRIVACY.md) for details.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Aion
|
|
2
|
+
|
|
3
|
+
**AI-powered calendar agent for Google Calendar.**
|
|
4
|
+
|
|
5
|
+
Schedule, list, reschedule, and find free time — all from natural language in your terminal.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
aion > schedule gym tomorrow morning
|
|
9
|
+
Finding optimal slot for 'gym'...
|
|
10
|
+
Schedule 'gym' on February 19, 2026 at 07:00 for 60 min? [y/n]: y
|
|
11
|
+
✔ Created! 'gym' on 2026-02-19 at 07:00
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Part of [A.U.R.A](https://github.com/sheikhmunim) (Autonomous Unified Reasoning Assistant).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Natural language** — "schedule dentist friday at 2pm for 45 min", "what's on tomorrow?"
|
|
21
|
+
- **Smart scheduling** — ASP/Clingo constraint solver finds optimal slots avoiding conflicts
|
|
22
|
+
- **Google Calendar sync** — reads and writes real events via Calendar API v3
|
|
23
|
+
- **Conflict detection** — warns on overlaps, offers alternatives
|
|
24
|
+
- **User preferences** — block time slots (lunch, sleep), set default morning/afternoon/evening
|
|
25
|
+
- **Timezone-aware** — auto-detects your timezone from Google Calendar on login
|
|
26
|
+
- **Ollama NLU** (optional) — local LLM fallback for complex commands, auto-installs on first run
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
User Input
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌──────────────┐ ┌──────────────┐
|
|
37
|
+
│ Regex NLU │───▶│ Ollama LLM │ (optional fallback)
|
|
38
|
+
│ (intent.py) │ │ (ollama.py) │
|
|
39
|
+
└──────┬───────┘ └──────────────┘
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
┌──────────────┐ ┌──────────────┐
|
|
43
|
+
│ ASP Solver │───▶│ Clingo │ (constraint solving)
|
|
44
|
+
│ (solver.py) │ │ │
|
|
45
|
+
└──────┬───────┘ └──────────────┘
|
|
46
|
+
│
|
|
47
|
+
▼
|
|
48
|
+
┌──────────────┐
|
|
49
|
+
│ Google Cal │ (httpx async)
|
|
50
|
+
│ (google_cal) │
|
|
51
|
+
└──────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install aion-agent
|
|
60
|
+
aion login
|
|
61
|
+
aion
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's it. `aion login` opens your browser for Google sign-in. Your timezone is auto-detected. No API keys or configuration needed.
|
|
65
|
+
|
|
66
|
+
On first run, Aion offers to install [Ollama](https://ollama.com) for smarter natural language understanding — this is optional.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
**From PyPI:**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install aion-agent
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**From source:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/sheikhmunim/Aion.git
|
|
82
|
+
cd Aion
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Requires **Python 3.10+**.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
Start the interactive CLI:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
aion
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Commands
|
|
99
|
+
|
|
100
|
+
| Action | Examples |
|
|
101
|
+
|--------|----------|
|
|
102
|
+
| **Schedule** | `schedule gym tomorrow morning`, `add meeting at 3pm for 90 min` |
|
|
103
|
+
| **List** | `what's on today?`, `show my calendar this week`, `what tomorrow?` |
|
|
104
|
+
| **Delete** | `cancel gym tomorrow`, `delete meeting` |
|
|
105
|
+
| **Update** | `move gym to 3pm`, `reschedule meeting to friday` |
|
|
106
|
+
| **Free slots** | `when am I free tomorrow?`, `free slots this week` |
|
|
107
|
+
| **Best time** | `best time for a 2h study session` |
|
|
108
|
+
| **Preferences** | `preferences` — manage blocked times and defaults |
|
|
109
|
+
| **Login/Logout** | `login`, `logout` |
|
|
110
|
+
| **Help** | `help` |
|
|
111
|
+
| **Quit** | `quit` or `exit` |
|
|
112
|
+
|
|
113
|
+
### Preferences
|
|
114
|
+
|
|
115
|
+
Block recurring time slots and set defaults:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
aion > preferences
|
|
119
|
+
┌─────────────────────────────────────────────┐
|
|
120
|
+
│ 1. Add a blocked time slot │
|
|
121
|
+
│ 2. Remove a blocked slot │
|
|
122
|
+
│ 3. Change default time preference │
|
|
123
|
+
│ 4. Back │
|
|
124
|
+
└─────────────────────────────────────────────┘
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Blocked slots (e.g. lunch 12:00-13:00 on weekdays) are respected by the scheduler — it won't suggest times during those windows.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Config lives at `~/.aion/config.json`. All options can also be set via environment variables with `AION_` prefix.
|
|
134
|
+
|
|
135
|
+
| Key | Env var | Default | Description |
|
|
136
|
+
|-----|---------|---------|-------------|
|
|
137
|
+
| `google_client_id` | `AION_GOOGLE_CLIENT_ID` | Built-in | OAuth client ID (override with your own if needed) |
|
|
138
|
+
| `google_client_secret` | `AION_GOOGLE_CLIENT_SECRET` | Built-in | OAuth client secret |
|
|
139
|
+
| `timezone` | `AION_TIMEZONE` | `UTC` | IANA timezone (auto-detected on login) |
|
|
140
|
+
| `default_duration` | `AION_DEFAULT_DURATION` | `60` | Default event duration in minutes |
|
|
141
|
+
| `ollama_url` | `AION_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
142
|
+
| `ollama_model` | `AION_OLLAMA_MODEL` | `qwen2.5:0.5b` | Ollama model for NLU |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install with dev dependencies
|
|
150
|
+
pip install -e ".[dev]"
|
|
151
|
+
|
|
152
|
+
# Run tests
|
|
153
|
+
pytest tests/ -v
|
|
154
|
+
|
|
155
|
+
# Lint
|
|
156
|
+
ruff check aion/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## How it works
|
|
162
|
+
|
|
163
|
+
1. **Intent classification** — Regex patterns match commands (schedule, list, delete, etc.) with confidence scores. Falls back to Ollama LLM for ambiguous input.
|
|
164
|
+
|
|
165
|
+
2. **Date parsing** — Handles "today", "tomorrow", weekday names, "this/next week", specific dates like "March 5th", and common typos.
|
|
166
|
+
|
|
167
|
+
3. **Constraint solving** — The ASP/Clingo solver models the day as 30-minute slots (6AM-10PM), marks busy times from existing events and user preferences, then finds optimal placements with time-of-day preferences.
|
|
168
|
+
|
|
169
|
+
4. **Google Calendar API** — All reads/writes go through Calendar API v3 via httpx async. Token refresh is automatic.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Dependencies
|
|
174
|
+
|
|
175
|
+
| Package | Purpose |
|
|
176
|
+
|---------|---------|
|
|
177
|
+
| [clingo](https://potassco.org/clingo/) | ASP constraint solver |
|
|
178
|
+
| [httpx](https://www.python-httpx.org/) | Async HTTP client (Google Calendar + Ollama) |
|
|
179
|
+
| [rich](https://rich.readthedocs.io/) | Terminal UI |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Privacy
|
|
184
|
+
|
|
185
|
+
Aion runs entirely on your machine. No calendar data is sent to external servers. See [PRIVACY.md](PRIVACY.md) for details.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""ASP Model for Calendar Scheduling — generates Answer Set Programs for Clingo."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from aion.config import get_now
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ASPModel:
|
|
9
|
+
"""Generates ASP rules for calendar scheduling.
|
|
10
|
+
|
|
11
|
+
Time model:
|
|
12
|
+
- Day divided into 30-minute slots (0-31)
|
|
13
|
+
- Slot 0 = 6:00 AM, Slot 31 = 9:30 PM
|
|
14
|
+
- Working hours: slots 6-24 (9:00 AM - 6:00 PM)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.slots_per_hour = 2 # 30-minute slots
|
|
19
|
+
self.day_start_hour = 6 # 6:00 AM
|
|
20
|
+
self.day_end_hour = 22 # 10:00 PM
|
|
21
|
+
self.total_slots = (self.day_end_hour - self.day_start_hour) * self.slots_per_hour
|
|
22
|
+
|
|
23
|
+
def time_to_slot(self, time_str: str) -> int:
|
|
24
|
+
h, m = map(int, time_str.split(":"))
|
|
25
|
+
return (h - self.day_start_hour) * self.slots_per_hour + (1 if m >= 30 else 0)
|
|
26
|
+
|
|
27
|
+
def slot_to_time(self, slot: int) -> str:
|
|
28
|
+
h = self.day_start_hour + slot // self.slots_per_hour
|
|
29
|
+
m = 30 if slot % self.slots_per_hour else 0
|
|
30
|
+
return f"{h:02d}:{m:02d}"
|
|
31
|
+
|
|
32
|
+
def duration_to_slots(self, minutes: int) -> int:
|
|
33
|
+
return (minutes + 29) // 30
|
|
34
|
+
|
|
35
|
+
def date_to_weekday(self, date_str: str) -> str:
|
|
36
|
+
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
|
37
|
+
return dt.strftime("%A").lower()
|
|
38
|
+
|
|
39
|
+
def get_week_dates(self, start_date: str | None = None) -> list[str]:
|
|
40
|
+
if start_date:
|
|
41
|
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
42
|
+
else:
|
|
43
|
+
start = get_now()
|
|
44
|
+
start = start - timedelta(days=start.weekday())
|
|
45
|
+
return [(start + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(7)]
|
|
46
|
+
|
|
47
|
+
def generate_base_program(self) -> str:
|
|
48
|
+
return f"""
|
|
49
|
+
% Time slots: 0 to {self.total_slots - 1} (30-min blocks from 6AM to 10PM)
|
|
50
|
+
time_slot(0..{self.total_slots - 1}).
|
|
51
|
+
|
|
52
|
+
% Days of the week
|
|
53
|
+
day(monday; tuesday; wednesday; thursday; friday; saturday; sunday).
|
|
54
|
+
|
|
55
|
+
% Day types
|
|
56
|
+
weekday(monday; tuesday; wednesday; thursday; friday).
|
|
57
|
+
weekend(saturday; sunday).
|
|
58
|
+
|
|
59
|
+
% Working hours: 9AM-6PM (slots 6-24)
|
|
60
|
+
working_hour(6..24).
|
|
61
|
+
|
|
62
|
+
% Morning: 6AM-12PM (slots 0-12)
|
|
63
|
+
morning(0..12).
|
|
64
|
+
|
|
65
|
+
% Afternoon: 12PM-6PM (slots 12-24)
|
|
66
|
+
afternoon(12..24).
|
|
67
|
+
|
|
68
|
+
% Evening: 6PM-10PM (slots 24-32)
|
|
69
|
+
evening(24..{self.total_slots - 1}).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def generate_busy_constraints(self, events: list[dict], dates: list[str] | None = None) -> str:
|
|
73
|
+
lines = ["\n% Busy times from existing events"]
|
|
74
|
+
for event in events:
|
|
75
|
+
if dates and event["date"] not in dates:
|
|
76
|
+
continue
|
|
77
|
+
weekday = self.date_to_weekday(event["date"])
|
|
78
|
+
start_slot = self.time_to_slot(event["time"])
|
|
79
|
+
duration_slots = self.duration_to_slots(event["duration"])
|
|
80
|
+
for slot in range(start_slot, min(start_slot + duration_slots, self.total_slots)):
|
|
81
|
+
lines.append(f'busy({weekday}, {slot}, "{event["date"]}").')
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
def generate_scheduling_request(self, request: dict) -> str:
|
|
85
|
+
activity = request.get("activity", "event").replace(" ", "_").lower()
|
|
86
|
+
duration_slots = self.duration_to_slots(request.get("duration", 60))
|
|
87
|
+
count = request.get("count", 1)
|
|
88
|
+
specific_date = request.get("date")
|
|
89
|
+
|
|
90
|
+
lines = [f"\n% Scheduling request: {activity}"]
|
|
91
|
+
lines.append(f'activity("{activity}").')
|
|
92
|
+
lines.append(f'duration("{activity}", {duration_slots}).')
|
|
93
|
+
lines.append(f'need_count("{activity}", {count}).')
|
|
94
|
+
|
|
95
|
+
if specific_date:
|
|
96
|
+
weekday = self.date_to_weekday(specific_date)
|
|
97
|
+
lines.append(f'allowed_day("{activity}", {weekday}).')
|
|
98
|
+
lines.append(f'target_date("{activity}", "{specific_date}").')
|
|
99
|
+
else:
|
|
100
|
+
allowed_days = request.get("days", [
|
|
101
|
+
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
|
|
102
|
+
])
|
|
103
|
+
for day in allowed_days:
|
|
104
|
+
lines.append(f'allowed_day("{activity}", {day}).')
|
|
105
|
+
|
|
106
|
+
lines.append(f"""
|
|
107
|
+
% Generate exactly {count} time slot(s) for {activity}
|
|
108
|
+
{count} {{ schedule("{activity}", D, T) : allowed_day("{activity}", D), time_slot(T) }} {count}.
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
lines.append(f"""
|
|
112
|
+
% Cannot overlap with busy times
|
|
113
|
+
:- schedule("{activity}", D, T), busy(D, T, _).
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
if duration_slots > 1:
|
|
117
|
+
lines.append(f"""
|
|
118
|
+
% Ensure full duration is available
|
|
119
|
+
:- schedule("{activity}", D, T), duration("{activity}", Dur),
|
|
120
|
+
Offset = 1..Dur-1, busy(D, T+Offset, _).
|
|
121
|
+
""")
|
|
122
|
+
|
|
123
|
+
lines.append(f"""
|
|
124
|
+
% Don't exceed end of day
|
|
125
|
+
:- schedule("{activity}", D, T), duration("{activity}", Dur), T + Dur > {self.total_slots}.
|
|
126
|
+
""")
|
|
127
|
+
|
|
128
|
+
if count > 1:
|
|
129
|
+
lines.append(f"""
|
|
130
|
+
% Spread sessions across different days
|
|
131
|
+
:- schedule("{activity}", D, T1), schedule("{activity}", D, T2), T1 != T2.
|
|
132
|
+
""")
|
|
133
|
+
|
|
134
|
+
if request.get("avoid_weekends"):
|
|
135
|
+
lines.append(f"""
|
|
136
|
+
:- schedule("{activity}", D, _), weekend(D).
|
|
137
|
+
""")
|
|
138
|
+
|
|
139
|
+
if request.get("working_hours_only"):
|
|
140
|
+
lines.append(f"""
|
|
141
|
+
:- schedule("{activity}", D, T), not working_hour(T).
|
|
142
|
+
""")
|
|
143
|
+
|
|
144
|
+
if request.get("prefer_morning"):
|
|
145
|
+
lines.append(f"""
|
|
146
|
+
#minimize {{ T@1,D : schedule("{activity}", D, T), not morning(T) }}.
|
|
147
|
+
#minimize {{ T@2,D : schedule("{activity}", D, T) }}.
|
|
148
|
+
""")
|
|
149
|
+
elif request.get("prefer_afternoon"):
|
|
150
|
+
lines.append(f"""
|
|
151
|
+
#minimize {{ 1@1,D,T : schedule("{activity}", D, T), not afternoon(T) }}.
|
|
152
|
+
""")
|
|
153
|
+
elif request.get("prefer_evening"):
|
|
154
|
+
lines.append(f"""
|
|
155
|
+
#minimize {{ 1@1,D,T : schedule("{activity}", D, T), not evening(T) }}.
|
|
156
|
+
""")
|
|
157
|
+
else:
|
|
158
|
+
lines.append(f"""
|
|
159
|
+
#minimize {{ T@1,D : schedule("{activity}", D, T) }}.
|
|
160
|
+
""")
|
|
161
|
+
|
|
162
|
+
lines.append("\n#show schedule/3.")
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
def generate_preference_constraints(
|
|
166
|
+
self, blocked_slots: list[dict], target_date: str | None = None
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Convert blocked preference slots into ASP busy facts.
|
|
169
|
+
|
|
170
|
+
If target_date is given, only generate facts for that date's weekday.
|
|
171
|
+
"""
|
|
172
|
+
today = get_now().strftime("%Y-%m-%d")
|
|
173
|
+
lines = ["\n% Blocked times from user preferences"]
|
|
174
|
+
|
|
175
|
+
for block in blocked_slots:
|
|
176
|
+
until = block.get("until")
|
|
177
|
+
if until and until < today:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
start_slot = self.time_to_slot(block["start"])
|
|
181
|
+
end_slot = self.time_to_slot(block["end"])
|
|
182
|
+
|
|
183
|
+
if target_date:
|
|
184
|
+
weekday = self.date_to_weekday(target_date)
|
|
185
|
+
if weekday not in block.get("days", []):
|
|
186
|
+
continue
|
|
187
|
+
for slot in range(start_slot, min(end_slot, self.total_slots)):
|
|
188
|
+
lines.append(f'busy({weekday}, {slot}, "preference").')
|
|
189
|
+
else:
|
|
190
|
+
for day in block.get("days", []):
|
|
191
|
+
for slot in range(start_slot, min(end_slot, self.total_slots)):
|
|
192
|
+
lines.append(f'busy({day}, {slot}, "preference").')
|
|
193
|
+
|
|
194
|
+
return "\n".join(lines)
|
|
195
|
+
|
|
196
|
+
def generate_full_program(self, events: list[dict], request: dict, dates: list[str] | None = None) -> str:
|
|
197
|
+
program = self.generate_base_program()
|
|
198
|
+
program += self.generate_busy_constraints(events, dates)
|
|
199
|
+
program += self.generate_scheduling_request(request)
|
|
200
|
+
return program
|