parcle 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.
- parcle-0.1.0/.gitignore +19 -0
- parcle-0.1.0/LICENSE +21 -0
- parcle-0.1.0/PKG-INFO +91 -0
- parcle-0.1.0/README.md +64 -0
- parcle-0.1.0/example.py +90 -0
- parcle-0.1.0/parcle/__init__.py +79 -0
- parcle-0.1.0/parcle/client.py +527 -0
- parcle-0.1.0/parcle/exceptions.py +151 -0
- parcle-0.1.0/parcle/models.py +222 -0
- parcle-0.1.0/parcle/py.typed +0 -0
- parcle-0.1.0/pyproject.toml +36 -0
- parcle-0.1.0/tests/test_client.py +432 -0
parcle-0.1.0/.gitignore
ADDED
parcle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parcle
|
|
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.
|
parcle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parcle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Long-term memory for AI agents — a Python client for the Parcle Memory API.
|
|
5
|
+
Project-URL: Homepage, https://parcle.ai
|
|
6
|
+
Project-URL: Documentation, https://api.parcle.ai
|
|
7
|
+
Author: Parcle
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,llm,memory,parcle,rag
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
|
|
30
|
+
# Parcle
|
|
31
|
+
|
|
32
|
+
**Long-term memory for AI agents**
|
|
33
|
+
|
|
34
|
+
Ingest conversations and files, then ask questions in natural language and get
|
|
35
|
+
cited answers back. Give every user a private, persistent agent memory.
|
|
36
|
+
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Why Parcle?
|
|
42
|
+
|
|
43
|
+
LLMs forget everything between calls. Parcle gives every user a private memory you
|
|
44
|
+
can write to and search:
|
|
45
|
+
|
|
46
|
+
- 🧠 **Per-user memory** — scope everything to a `user_id`.
|
|
47
|
+
- 💬 **Ingest anything** — chat transcripts and files (PDF, Markdown, text, …) go in the same place.
|
|
48
|
+
- 🔎 **Ask, don't query** — search returns a synthesized **answer** with **citations**, not just raw chunks.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install parcle
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from parcle import Parcle
|
|
60
|
+
|
|
61
|
+
# Reads PARCLE_API_KEY from the environment if api_key is omitted.
|
|
62
|
+
client = Parcle(api_key="pk_live_...")
|
|
63
|
+
|
|
64
|
+
# 1. Write a conversation into a user's memory.
|
|
65
|
+
# Ingestion is incremental: omit session_id to start a new session, then
|
|
66
|
+
# pass the returned session_id back to append more turns to the same one.
|
|
67
|
+
result = client.ingest_dialog(
|
|
68
|
+
user_id="ada",
|
|
69
|
+
messages=[
|
|
70
|
+
{"role": "user", "content": "I'm allergic to peanuts."},
|
|
71
|
+
{"role": "assistant", "content": "Got it — I'll avoid peanuts in suggestions."},
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
client.ingest_dialog(
|
|
75
|
+
user_id="ada",
|
|
76
|
+
session_id=result.session_id, # append to the same session
|
|
77
|
+
messages=[
|
|
78
|
+
{"role": "user", "content": "Also, I don't eat shellfish."},
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 2. ...or ingest a file (PDF, Markdown, text, …).
|
|
83
|
+
client.ingest_file(user_id="ada", file="diet-notes.pdf")
|
|
84
|
+
|
|
85
|
+
# 3. Ask a question. You get an answer with confidence and citations.
|
|
86
|
+
result = client.search(user_id="ada", query="What food should I avoid?")
|
|
87
|
+
|
|
88
|
+
print(result.answer) # "You're allergic to peanuts, so avoid them."
|
|
89
|
+
print(result.confidence) # 0.92
|
|
90
|
+
print(result.citations) # [Citation(type="session", id="...")]
|
|
91
|
+
```
|
parcle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Parcle
|
|
4
|
+
|
|
5
|
+
**Long-term memory for AI agents**
|
|
6
|
+
|
|
7
|
+
Ingest conversations and files, then ask questions in natural language and get
|
|
8
|
+
cited answers back. Give every user a private, persistent agent memory.
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why Parcle?
|
|
15
|
+
|
|
16
|
+
LLMs forget everything between calls. Parcle gives every user a private memory you
|
|
17
|
+
can write to and search:
|
|
18
|
+
|
|
19
|
+
- 🧠 **Per-user memory** — scope everything to a `user_id`.
|
|
20
|
+
- 💬 **Ingest anything** — chat transcripts and files (PDF, Markdown, text, …) go in the same place.
|
|
21
|
+
- 🔎 **Ask, don't query** — search returns a synthesized **answer** with **citations**, not just raw chunks.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install parcle
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from parcle import Parcle
|
|
33
|
+
|
|
34
|
+
# Reads PARCLE_API_KEY from the environment if api_key is omitted.
|
|
35
|
+
client = Parcle(api_key="pk_live_...")
|
|
36
|
+
|
|
37
|
+
# 1. Write a conversation into a user's memory.
|
|
38
|
+
# Ingestion is incremental: omit session_id to start a new session, then
|
|
39
|
+
# pass the returned session_id back to append more turns to the same one.
|
|
40
|
+
result = client.ingest_dialog(
|
|
41
|
+
user_id="ada",
|
|
42
|
+
messages=[
|
|
43
|
+
{"role": "user", "content": "I'm allergic to peanuts."},
|
|
44
|
+
{"role": "assistant", "content": "Got it — I'll avoid peanuts in suggestions."},
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
client.ingest_dialog(
|
|
48
|
+
user_id="ada",
|
|
49
|
+
session_id=result.session_id, # append to the same session
|
|
50
|
+
messages=[
|
|
51
|
+
{"role": "user", "content": "Also, I don't eat shellfish."},
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 2. ...or ingest a file (PDF, Markdown, text, …).
|
|
56
|
+
client.ingest_file(user_id="ada", file="diet-notes.pdf")
|
|
57
|
+
|
|
58
|
+
# 3. Ask a question. You get an answer with confidence and citations.
|
|
59
|
+
result = client.search(user_id="ada", query="What food should I avoid?")
|
|
60
|
+
|
|
61
|
+
print(result.answer) # "You're allergic to peanuts, so avoid them."
|
|
62
|
+
print(result.confidence) # 0.92
|
|
63
|
+
print(result.citations) # [Citation(type="session", id="...")]
|
|
64
|
+
```
|
parcle-0.1.0/example.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""End-to-end example against the live Parcle Memory API.
|
|
2
|
+
|
|
3
|
+
Run it with the SDK installed (``pip install -e .``)::
|
|
4
|
+
|
|
5
|
+
python example.py
|
|
6
|
+
|
|
7
|
+
It walks the full memory lifecycle for one user: create the user, ingest a
|
|
8
|
+
dialog and a file, wait for both to become searchable, ask a question, then
|
|
9
|
+
inspect and clean up the stored sources.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import tempfile
|
|
16
|
+
|
|
17
|
+
from parcle import Parcle
|
|
18
|
+
|
|
19
|
+
# The API key and base URL. Prefer env vars; fall back to the literals so this
|
|
20
|
+
# example is runnable as-is. This key lives on the backend.parcle.ai environment.
|
|
21
|
+
API_KEY = os.environ.get("PARCLE_API_KEY", "pmem_9je7QBamPK9b3a0qFXCvCMkk5U9iQaIrkGp4lMyi9Kg")
|
|
22
|
+
BASE_URL = os.environ.get("PARCLE_BASE_URL", "https://backend.parcle.ai")
|
|
23
|
+
|
|
24
|
+
USER_ID = "ada"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
# Search synthesizes an answer with an LLM, so allow a generous timeout.
|
|
29
|
+
with Parcle(api_key=API_KEY, base_url=BASE_URL, timeout=120.0) as client:
|
|
30
|
+
# 1. Create (or update) the user this memory belongs to.
|
|
31
|
+
user = client.create_user(USER_ID, name="Ada", timezone="America/New_York")
|
|
32
|
+
print(f"user: {user.user_id} (is_new={user.is_new})")
|
|
33
|
+
|
|
34
|
+
# 2. Ingest a conversation. Ingestion is incremental: keep the
|
|
35
|
+
# session_id to append more turns to the same session later.
|
|
36
|
+
dialog = client.ingest_dialog(
|
|
37
|
+
user_id=USER_ID,
|
|
38
|
+
messages=[
|
|
39
|
+
{"role": "user", "content": "I'm allergic to peanuts."},
|
|
40
|
+
{"role": "assistant", "content": "Got it — I'll avoid peanuts."},
|
|
41
|
+
],
|
|
42
|
+
tag={"app": "meal-planner"},
|
|
43
|
+
)
|
|
44
|
+
print(f"dialog: session={dialog.session_id} event={dialog.event_id}")
|
|
45
|
+
|
|
46
|
+
client.ingest_dialog(
|
|
47
|
+
user_id=USER_ID,
|
|
48
|
+
session_id=dialog.session_id, # append to the same session
|
|
49
|
+
messages=[{"role": "user", "content": "Also, I don't eat shellfish."}],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# 3. Ingest a file into the same memory.
|
|
53
|
+
with tempfile.NamedTemporaryFile(
|
|
54
|
+
"w", suffix=".md", delete=False, encoding="utf-8"
|
|
55
|
+
) as fh:
|
|
56
|
+
fh.write("# Diet notes\n\nVegetarian. Avoid nuts and shellfish.\n")
|
|
57
|
+
notes_path = fh.name
|
|
58
|
+
try:
|
|
59
|
+
uploaded = client.ingest_file(
|
|
60
|
+
user_id=USER_ID, file=notes_path, tag={"source": "diet-notes"}
|
|
61
|
+
)
|
|
62
|
+
print(f"file: id={uploaded.file_id} event={uploaded.event_id}")
|
|
63
|
+
|
|
64
|
+
# 4. Wait until both writes are searchable.
|
|
65
|
+
client.wait_until_ready(USER_ID, dialog.event_id)
|
|
66
|
+
client.wait_until_ready(USER_ID, uploaded.event_id)
|
|
67
|
+
print("ingestion ready")
|
|
68
|
+
|
|
69
|
+
# 5. Ask a question — you get an answer with confidence + citations.
|
|
70
|
+
result = client.search(
|
|
71
|
+
user_id=USER_ID,
|
|
72
|
+
query="What food should I avoid?",
|
|
73
|
+
tag_filter={"app": "meal-planner"},
|
|
74
|
+
)
|
|
75
|
+
print(f"\nanswer: {result.answer}")
|
|
76
|
+
print(f"confidence: {result.confidence}")
|
|
77
|
+
for c in result.citations:
|
|
78
|
+
print(f"citation: {c.type} {c.id}")
|
|
79
|
+
|
|
80
|
+
# 6. Inspect what's stored for this user.
|
|
81
|
+
print("\nsources:")
|
|
82
|
+
for source in client.iter_sources(USER_ID):
|
|
83
|
+
label = source.name or source.id
|
|
84
|
+
print(f" - {source.type}: {label} tag={source.tag}")
|
|
85
|
+
finally:
|
|
86
|
+
os.unlink(notes_path)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Parcle — long-term memory for AI agents.
|
|
2
|
+
|
|
3
|
+
Ingest conversations and files into a per-user memory, then ask questions in
|
|
4
|
+
natural language and get cited answers back.
|
|
5
|
+
|
|
6
|
+
from parcle import Parcle
|
|
7
|
+
|
|
8
|
+
client = Parcle(api_key="pk_live_...")
|
|
9
|
+
client.ingest_dialog(user_id="ada", messages=[{"role": "user", "content": "..."}])
|
|
10
|
+
result = client.search(user_id="ada", query="What food should I avoid?")
|
|
11
|
+
print(result.answer, result.confidence, result.citations)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .client import Parcle
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
FileTooLargeError,
|
|
20
|
+
InternalServerError,
|
|
21
|
+
InvalidRequestError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
ParcleAPIError,
|
|
24
|
+
ParcleConfigError,
|
|
25
|
+
ParcleConnectionError,
|
|
26
|
+
ParcleError,
|
|
27
|
+
ParcleTimeoutError,
|
|
28
|
+
RateLimitError,
|
|
29
|
+
ServiceUnavailableError,
|
|
30
|
+
UnsupportedFileTypeError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
)
|
|
33
|
+
from .models import (
|
|
34
|
+
Citation,
|
|
35
|
+
DeleteResult,
|
|
36
|
+
Event,
|
|
37
|
+
IngestDialogResult,
|
|
38
|
+
IngestFileResult,
|
|
39
|
+
Message,
|
|
40
|
+
SearchResult,
|
|
41
|
+
Session,
|
|
42
|
+
Source,
|
|
43
|
+
SourcesPage,
|
|
44
|
+
User,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Parcle",
|
|
51
|
+
"__version__",
|
|
52
|
+
# models
|
|
53
|
+
"Citation",
|
|
54
|
+
"DeleteResult",
|
|
55
|
+
"Event",
|
|
56
|
+
"IngestDialogResult",
|
|
57
|
+
"IngestFileResult",
|
|
58
|
+
"Message",
|
|
59
|
+
"SearchResult",
|
|
60
|
+
"Session",
|
|
61
|
+
"Source",
|
|
62
|
+
"SourcesPage",
|
|
63
|
+
"User",
|
|
64
|
+
# exceptions
|
|
65
|
+
"ParcleError",
|
|
66
|
+
"ParcleConfigError",
|
|
67
|
+
"ParcleConnectionError",
|
|
68
|
+
"ParcleTimeoutError",
|
|
69
|
+
"ParcleAPIError",
|
|
70
|
+
"InvalidRequestError",
|
|
71
|
+
"AuthenticationError",
|
|
72
|
+
"NotFoundError",
|
|
73
|
+
"FileTooLargeError",
|
|
74
|
+
"UnsupportedFileTypeError",
|
|
75
|
+
"ValidationError",
|
|
76
|
+
"RateLimitError",
|
|
77
|
+
"InternalServerError",
|
|
78
|
+
"ServiceUnavailableError",
|
|
79
|
+
]
|