item-extractor 1.0.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.
- item_extractor-1.0.0/PKG-INFO +92 -0
- item_extractor-1.0.0/README.md +72 -0
- item_extractor-1.0.0/pyproject.toml +51 -0
- item_extractor-1.0.0/pyproject.toml.orig +51 -0
- item_extractor-1.0.0/src/item_extractor/__init__.py +28 -0
- item_extractor-1.0.0/src/item_extractor/extractor.py +143 -0
- item_extractor-1.0.0/src/item_extractor/models.py +52 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: item-extractor
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Extract todos, tasks, reminders, and events from natural language
|
|
5
|
+
Author: UnknwnDev
|
|
6
|
+
Author-email: UnknwnDev <0unknwn.dev@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: dateparser>=1.4.3
|
|
12
|
+
Requires-Dist: spacy>=3.8.16
|
|
13
|
+
Requires-Dist: ipykernel>=7.3.0 ; extra == 'notebook'
|
|
14
|
+
Requires-Dist: pandas>=3.0.6 ; extra == 'notebook'
|
|
15
|
+
Requires-Dist: scikit-learn>=1.9.1 ; extra == 'notebook'
|
|
16
|
+
Requires-Dist: pytest>=9.1.1 ; extra == 'notebook'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Provides-Extra: notebook
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Item Extractor
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org)
|
|
24
|
+
[](https://pypi.org)
|
|
25
|
+
|
|
26
|
+
lightweight feature extraction that turns natural-language into structured JSON of type tasks, events, todos, etc...
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Choose the installation method that fits your use case:
|
|
31
|
+
|
|
32
|
+
### 1. Standard Installation (Production / Inference)
|
|
33
|
+
If you only need to run existing agents, install the core package. This keeps the installation lightweight and bundles the standard **spaCy `en_core_web_md` model** automatically:
|
|
34
|
+
```bash
|
|
35
|
+
uv add item_extractor
|
|
36
|
+
# or via pip
|
|
37
|
+
pip install item_extractor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Notebook Installation (Development / Intent Classification Custom Agent Training)
|
|
41
|
+
If you want to use our interactive Jupyter Notebooks to design, benchmark, and train your own custom agent models, install the package with the `notebook` extra dependencies:
|
|
42
|
+
```bash
|
|
43
|
+
uv add "item_extractor[notebook]"
|
|
44
|
+
# or via pip
|
|
45
|
+
pip install "item_extractor[notebook]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🚀 Quick Start (Core Library)
|
|
51
|
+
|
|
52
|
+
Use the built-in components to run an agent directly in your Python application:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from item_extractor import extract
|
|
56
|
+
|
|
57
|
+
items = extract("Organize the garage")
|
|
58
|
+
print(response) # Returns Todo(title='Organize the garage', description='', type=<ItemType.TODO: 'todo'>, completed=False)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🧠 Creating Custom Intent Classification Agent Model
|
|
64
|
+
|
|
65
|
+
If you installed the package with the `[notebook]` extras, you can create and fine-tune your own agent architectures.
|
|
66
|
+
|
|
67
|
+
1. Clone this repository to access the starter templates:
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/UnknwnDev/item-extractor.git
|
|
70
|
+
cd item_extractor
|
|
71
|
+
```
|
|
72
|
+
2. Open `notebooks/create_custom_agent.ipynb` and follow the step-by-step guide to train your agent using spaCy embeddings, customize decision thresholds, and evaluate agent trajectories.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## 🛠️ Contribution & Local Setup
|
|
77
|
+
|
|
78
|
+
For developers looking to contribute to the codebase:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Clone and sync all environment dependencies including notebook extras
|
|
82
|
+
git clone https://github.com/UnknwnDev/item-extractor.git
|
|
83
|
+
cd item_extractor
|
|
84
|
+
uv sync --extra notebook
|
|
85
|
+
|
|
86
|
+
# Run the test suite
|
|
87
|
+
uv run pytest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 📄 License
|
|
91
|
+
|
|
92
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Item Extractor
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org)
|
|
4
|
+
[](https://pypi.org)
|
|
5
|
+
|
|
6
|
+
lightweight feature extraction that turns natural-language into structured JSON of type tasks, events, todos, etc...
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Choose the installation method that fits your use case:
|
|
11
|
+
|
|
12
|
+
### 1. Standard Installation (Production / Inference)
|
|
13
|
+
If you only need to run existing agents, install the core package. This keeps the installation lightweight and bundles the standard **spaCy `en_core_web_md` model** automatically:
|
|
14
|
+
```bash
|
|
15
|
+
uv add item_extractor
|
|
16
|
+
# or via pip
|
|
17
|
+
pip install item_extractor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Notebook Installation (Development / Intent Classification Custom Agent Training)
|
|
21
|
+
If you want to use our interactive Jupyter Notebooks to design, benchmark, and train your own custom agent models, install the package with the `notebook` extra dependencies:
|
|
22
|
+
```bash
|
|
23
|
+
uv add "item_extractor[notebook]"
|
|
24
|
+
# or via pip
|
|
25
|
+
pip install "item_extractor[notebook]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Quick Start (Core Library)
|
|
31
|
+
|
|
32
|
+
Use the built-in components to run an agent directly in your Python application:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from item_extractor import extract
|
|
36
|
+
|
|
37
|
+
items = extract("Organize the garage")
|
|
38
|
+
print(response) # Returns Todo(title='Organize the garage', description='', type=<ItemType.TODO: 'todo'>, completed=False)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🧠 Creating Custom Intent Classification Agent Model
|
|
44
|
+
|
|
45
|
+
If you installed the package with the `[notebook]` extras, you can create and fine-tune your own agent architectures.
|
|
46
|
+
|
|
47
|
+
1. Clone this repository to access the starter templates:
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/UnknwnDev/item-extractor.git
|
|
50
|
+
cd item_extractor
|
|
51
|
+
```
|
|
52
|
+
2. Open `notebooks/create_custom_agent.ipynb` and follow the step-by-step guide to train your agent using spaCy embeddings, customize decision thresholds, and evaluate agent trajectories.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## 🛠️ Contribution & Local Setup
|
|
57
|
+
|
|
58
|
+
For developers looking to contribute to the codebase:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Clone and sync all environment dependencies including notebook extras
|
|
62
|
+
git clone https://github.com/UnknwnDev/item-extractor.git
|
|
63
|
+
cd item_extractor
|
|
64
|
+
uv sync --extra notebook
|
|
65
|
+
|
|
66
|
+
# Run the test suite
|
|
67
|
+
uv run pytest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 📄 License
|
|
71
|
+
|
|
72
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "item-extractor"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Extract todos, tasks, reminders, and events from natural language"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"License :: OSI Approved :: MIT License",
|
|
10
|
+
"Operating System :: OS Independent",
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"dateparser>=1.4.3",
|
|
14
|
+
"spacy>=3.8.16",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[project.authors]]
|
|
18
|
+
name = "UnknwnDev"
|
|
19
|
+
email = "0unknwn.dev@gmail.com"
|
|
20
|
+
|
|
21
|
+
[project.license]
|
|
22
|
+
text = "MIT"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
notebook = [
|
|
26
|
+
"ipykernel>=7.3.0",
|
|
27
|
+
"pandas>=3.0.6",
|
|
28
|
+
"scikit-learn>=1.9.1",
|
|
29
|
+
"pytest>=9.1.1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources.en_core_web_md]
|
|
33
|
+
url = "https://github.com"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
exclude = [
|
|
37
|
+
"*.ipynb",
|
|
38
|
+
"notebooks/",
|
|
39
|
+
"tests/",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
exclude = [
|
|
44
|
+
"*.ipynb",
|
|
45
|
+
"notebooks/",
|
|
46
|
+
"tests/",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
51
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "item-extractor"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Extract todos, tasks, reminders, and events from natural language"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "UnknwnDev", email = "0unknwn.dev@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"dateparser>=1.4.3",
|
|
19
|
+
"spacy>=3.8.16",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.uv.sources]
|
|
23
|
+
en_core_web_md = { url = "https://github.com" }
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
notebook = [
|
|
27
|
+
"ipykernel>=7.3.0",
|
|
28
|
+
"pandas>=3.0.6",
|
|
29
|
+
"scikit-learn>=1.9.1",
|
|
30
|
+
"pytest>=9.1.1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
# Exclude notebooks from the final .whl package
|
|
35
|
+
exclude = [
|
|
36
|
+
"*.ipynb",
|
|
37
|
+
"notebooks/",
|
|
38
|
+
"tests/",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
# Exclude notebooks from the source distribution archive (.tar.gz)
|
|
43
|
+
exclude = [
|
|
44
|
+
"*.ipynb",
|
|
45
|
+
"notebooks/",
|
|
46
|
+
"tests/",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
51
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .extractor import Extractor
|
|
2
|
+
from .models import (
|
|
3
|
+
Event,
|
|
4
|
+
ExtractedItem,
|
|
5
|
+
ItemType,
|
|
6
|
+
Reminder,
|
|
7
|
+
Task,
|
|
8
|
+
Todo,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Create a default instance on module load
|
|
12
|
+
_default_extractor = Extractor()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract(text: str) -> list[ExtractedItem]:
|
|
16
|
+
return _default_extractor.extract(text)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"extract",
|
|
21
|
+
"Extractor",
|
|
22
|
+
"ExtractedItem",
|
|
23
|
+
"ItemType",
|
|
24
|
+
"Todo",
|
|
25
|
+
"Task",
|
|
26
|
+
"Reminder",
|
|
27
|
+
"Event",
|
|
28
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import dateparser
|
|
2
|
+
import joblib
|
|
3
|
+
import spacy
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from spacy.matcher import Matcher
|
|
7
|
+
|
|
8
|
+
from .models import *
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Extractor:
|
|
12
|
+
def __init__(self, model="en_core_web_md", timezone="PST") -> None:
|
|
13
|
+
self.classifier = joblib.load("model/intent_classifier.joblib")
|
|
14
|
+
self.nlp = spacy.load(model)
|
|
15
|
+
self.matcher = Matcher(self.nlp.vocab)
|
|
16
|
+
self.timezone = timezone
|
|
17
|
+
|
|
18
|
+
todo_pattern = [
|
|
19
|
+
{"POS": "VERB"},
|
|
20
|
+
{"POS": "DET", "OP": "?"},
|
|
21
|
+
{"POS": "ADJ", "OP": "*"},
|
|
22
|
+
{"POS": "NOUN", "OP": "*"},
|
|
23
|
+
{"POS": "NOUN"},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
task_pattern = [
|
|
27
|
+
{"POS": "VERB"},
|
|
28
|
+
{"POS": {"IN": ["DET", "ADJ", "NOUN", "PROPN", "NUM"]}, "OP": "*"},
|
|
29
|
+
{"POS": "NOUN"},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
reminder_pattern = [
|
|
33
|
+
{"LOWER": "remind"},
|
|
34
|
+
{"POS": {"IN": ["PRON", "PROPN"]}},
|
|
35
|
+
{"LOWER": "to"},
|
|
36
|
+
{"POS": "VERB"},
|
|
37
|
+
{"OP": "{1,4}"}, # Captures the next 1-4 words
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
event_pattern = [
|
|
41
|
+
{"POS": {"IN": ["NOUN", "ADJ", "PROPN"]}, "OP": "*"},
|
|
42
|
+
{"LEMMA": {"IN": ["appointment", "meeting", "lunch", "event", "session"]}},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
self.matcher.add("TODO", [todo_pattern])
|
|
46
|
+
self.matcher.add("TASK", [task_pattern])
|
|
47
|
+
self.matcher.add("REMINDER", [reminder_pattern])
|
|
48
|
+
self.matcher.add("EVENT", [event_pattern])
|
|
49
|
+
|
|
50
|
+
def parse_command(self, text: str):
|
|
51
|
+
"""Extracts item title, datetime, and location from given text for item creation.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
text (str): user input
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
dict: dictionary of title, datetime, and location
|
|
58
|
+
"""
|
|
59
|
+
doc = self.nlp(text)
|
|
60
|
+
matches = self.matcher(doc)
|
|
61
|
+
|
|
62
|
+
if not matches:
|
|
63
|
+
return {"title": "Could not extract a pattern."}
|
|
64
|
+
|
|
65
|
+
longest_match = None
|
|
66
|
+
max_length = 0
|
|
67
|
+
|
|
68
|
+
for match_id, start, end in matches:
|
|
69
|
+
match_length = end - start
|
|
70
|
+
if match_length > max_length:
|
|
71
|
+
max_length = match_length
|
|
72
|
+
longest_match = (match_id, start, end)
|
|
73
|
+
|
|
74
|
+
match_id, start, end = longest_match # type: ignore
|
|
75
|
+
intent_label = self.nlp.vocab.strings[match_id]
|
|
76
|
+
|
|
77
|
+
if intent_label == "REMINDER":
|
|
78
|
+
match_span = doc[start:end]
|
|
79
|
+
to_index = start + 2
|
|
80
|
+
|
|
81
|
+
for token in match_span:
|
|
82
|
+
if token.lower_ == "to":
|
|
83
|
+
to_index = token.i
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
extracted_phrase = doc[to_index + 1 : end].text
|
|
87
|
+
else:
|
|
88
|
+
extracted_phrase = doc[start:end].text
|
|
89
|
+
|
|
90
|
+
date: str = ""
|
|
91
|
+
time: str = ""
|
|
92
|
+
location: str | None = None
|
|
93
|
+
|
|
94
|
+
for ent in doc.ents:
|
|
95
|
+
if ent.label_ == "DATE":
|
|
96
|
+
date = ent.text
|
|
97
|
+
if ent.label_ == "TIME":
|
|
98
|
+
time = ent.text
|
|
99
|
+
if ent.label_ in ["GPE", "LOC"]:
|
|
100
|
+
location = ent.text
|
|
101
|
+
|
|
102
|
+
date_time: datetime | None = dateparser.parse(
|
|
103
|
+
date + time, settings={"TIMEZONE": self.timezone}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
data = {"title": extracted_phrase, "datetime": date_time, "location": location}
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
def extract(self, text: str) -> ExtractedItem:
|
|
110
|
+
"""Extract todo, task, reminder, and event from natural language.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
text (str): user input
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
ExtractedItem: Object of Todo | Task | Reminder | Event | None
|
|
117
|
+
"""
|
|
118
|
+
# Predict the category without any hardcoded rules
|
|
119
|
+
prediction = self.classifier.predict([text])[0]
|
|
120
|
+
|
|
121
|
+
item = None
|
|
122
|
+
|
|
123
|
+
data = self.parse_command(text)
|
|
124
|
+
|
|
125
|
+
print(data)
|
|
126
|
+
if prediction == "todo":
|
|
127
|
+
item = Todo(title=data["title"])
|
|
128
|
+
elif prediction == "reminder":
|
|
129
|
+
item = Reminder(title=data["title"])
|
|
130
|
+
elif prediction == "event":
|
|
131
|
+
item = Event(
|
|
132
|
+
title=data["title"],
|
|
133
|
+
start_at=data["datetime"], # type: ignore
|
|
134
|
+
end_at=data["datetime"] + timedelta(hours=1), # type: ignore
|
|
135
|
+
location=data["location"]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif prediction == "task":
|
|
139
|
+
item = Task(title=data["title"], due_at=data["datetime"]) # type: ignore
|
|
140
|
+
|
|
141
|
+
print(item, prediction)
|
|
142
|
+
|
|
143
|
+
return item
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ItemType(StrEnum):
|
|
9
|
+
TODO = "todo"
|
|
10
|
+
TASK = "task"
|
|
11
|
+
REMINDER = "reminder"
|
|
12
|
+
EVENT = "event"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseItem(BaseModel):
|
|
16
|
+
title: str
|
|
17
|
+
description: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Todo(BaseItem):
|
|
21
|
+
type: Literal[ItemType.TODO] = ItemType.TODO
|
|
22
|
+
|
|
23
|
+
completed: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Task(BaseItem):
|
|
27
|
+
type: Literal[ItemType.TASK] = ItemType.TASK
|
|
28
|
+
|
|
29
|
+
due_at: datetime | None = None
|
|
30
|
+
priority: int | None = Field(default=None, ge=1, le=5)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Reminder(BaseItem):
|
|
34
|
+
type: Literal[ItemType.REMINDER] = ItemType.REMINDER
|
|
35
|
+
|
|
36
|
+
remind_at: datetime | None = None
|
|
37
|
+
recurrence: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Event(BaseItem):
|
|
41
|
+
type: Literal[ItemType.EVENT] = ItemType.EVENT
|
|
42
|
+
|
|
43
|
+
start_at: datetime
|
|
44
|
+
end_at: datetime | None = None
|
|
45
|
+
location: str | None = None
|
|
46
|
+
attendees: list[str] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ExtractedItem = Annotated[
|
|
50
|
+
Todo|Task|Reminder|Event|None,
|
|
51
|
+
Field(discriminator="type"),
|
|
52
|
+
]
|