praixis 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.
- praixis-0.1.0/.claude/settings.local.json +17 -0
- praixis-0.1.0/.gitignore +21 -0
- praixis-0.1.0/LICENSE +21 -0
- praixis-0.1.0/PKG-INFO +171 -0
- praixis-0.1.0/README.md +144 -0
- praixis-0.1.0/praixis/__init__.py +63 -0
- praixis-0.1.0/praixis/_files.py +45 -0
- praixis-0.1.0/praixis/_http.py +77 -0
- praixis-0.1.0/praixis/_transport.py +133 -0
- praixis-0.1.0/praixis/aio/__init__.py +10 -0
- praixis-0.1.0/praixis/aio/_transport.py +123 -0
- praixis-0.1.0/praixis/aio/client.py +52 -0
- praixis-0.1.0/praixis/aio/resources/__init__.py +10 -0
- praixis-0.1.0/praixis/aio/resources/chat.py +86 -0
- praixis-0.1.0/praixis/aio/resources/rag.py +113 -0
- praixis-0.1.0/praixis/client.py +39 -0
- praixis-0.1.0/praixis/errors.py +60 -0
- praixis-0.1.0/praixis/py.typed +0 -0
- praixis-0.1.0/praixis/resources/__init__.py +6 -0
- praixis-0.1.0/praixis/resources/chat.py +86 -0
- praixis-0.1.0/praixis/resources/rag.py +113 -0
- praixis-0.1.0/praixis/types.py +53 -0
- praixis-0.1.0/pyproject.toml +40 -0
- praixis-0.1.0/tests/test_async_client.py +185 -0
- praixis-0.1.0/tests/test_client.py +162 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(ls \"C:\\\\Users\\\\meramirez\\\\Desktop\\\\PythonProjects\\\\PraixisEngine\")",
|
|
5
|
+
"Read(//c/Users/meramirez/Desktop/PythonProjects/**)",
|
|
6
|
+
"Bash(uv run *)",
|
|
7
|
+
"Bash(PRAIXIS_KEY='praixis_HZU7igk-i6LPxzPJNPv6V-3iA3O8qDBprRbkZOoTKog' PRAIXIS_URL='http://172.30.1.144:8080' uv run --no-project python _live_check.py)",
|
|
8
|
+
"Bash(rm -f praixis/resources/admin.py praixis/aio/resources/admin.py)",
|
|
9
|
+
"Bash(rm -f praixis/resources/__pycache__/admin*.pyc praixis/aio/resources/__pycache__/admin*.pyc)",
|
|
10
|
+
"Bash(PRAIXIS_KEY=praixis_HZU7igk-i6LPxzPJNPv6V-3iA3O8qDBprRbkZOoTKog uv run --no-project python -c ' *)",
|
|
11
|
+
"Bash(New-Item -ItemType File praixis/py.typed)",
|
|
12
|
+
"Bash(Out-Null)",
|
|
13
|
+
"Bash(Test-Path praixis/py.typed)",
|
|
14
|
+
"Bash(uv build *)"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
praixis-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
uv.lock
|
|
13
|
+
|
|
14
|
+
# Editor / OS
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
.DS_Store
|
|
18
|
+
Thumbs.db
|
|
19
|
+
|
|
20
|
+
# Internal notes (not for distribution)
|
|
21
|
+
NOTES.md
|
praixis-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael E. Ramirez A.
|
|
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.
|
praixis-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: praixis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency Python client for the Praixis Engine API
|
|
5
|
+
Project-URL: Homepage, https://github.com/mettjs/praixis-python
|
|
6
|
+
Project-URL: Repository, https://github.com/mettjs/praixis-python
|
|
7
|
+
Project-URL: Issues, https://github.com/mettjs/praixis-python/issues
|
|
8
|
+
Author: Michael E. Ramirez A.
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,llm,praixis,rag
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: async
|
|
25
|
+
Requires-Dist: httpx>=0.24; extra == 'async'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Praixis Engine — Python Client
|
|
29
|
+
|
|
30
|
+
A lightweight Python client for the Praixis Engine API, in both **sync** and
|
|
31
|
+
**async** flavors.
|
|
32
|
+
|
|
33
|
+
- **`PraixisClient`** — synchronous, **zero dependencies**, built entirely on
|
|
34
|
+
the standard library (`urllib`, `json`, `uuid`), so an upstream package
|
|
35
|
+
release can never break it.
|
|
36
|
+
- **`AsyncPraixisClient`** — async/await, built on `httpx` (the only optional
|
|
37
|
+
dependency). Imported lazily, so the sync client stays dependency-free.
|
|
38
|
+
- Same surface in both: resource-grouped `client.chat` and `client.rag` — chat
|
|
39
|
+
+ file summary, and RAG ingest/ask/embed/compare.
|
|
40
|
+
|
|
41
|
+
> The companion Node.js client lives in its own repository.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install praixis # sync client only, zero dependencies
|
|
47
|
+
pip install "praixis[async]" # also installs httpx for the async client
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or vendor the `praixis/` package directly into your project — the sync client
|
|
51
|
+
has no deps.
|
|
52
|
+
|
|
53
|
+
Requires Python 3.10+.
|
|
54
|
+
|
|
55
|
+
## Authentication
|
|
56
|
+
|
|
57
|
+
Every endpoint uses an app-level API key sent as the `X-API-Key` header.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from praixis import PraixisClient
|
|
61
|
+
|
|
62
|
+
client = PraixisClient("http://localhost:8080", "your-api-key")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> Admin/system routes (`/api/system/*`, HTTP Basic auth) are intentionally **not**
|
|
66
|
+
> part of this SDK. They already have a browser UI, and baking admin credentials
|
|
67
|
+
> into application code would be a security anti-pattern. The `X-API-Key` this
|
|
68
|
+
> SDK uses is an app-level credential, not an admin one.
|
|
69
|
+
|
|
70
|
+
## Chat
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Start a conversation
|
|
74
|
+
reply = client.chat.send("Hello, world!")
|
|
75
|
+
print(reply["session_id"], reply["response"])
|
|
76
|
+
|
|
77
|
+
# Continue it
|
|
78
|
+
client.chat.send("And again?", session_id=reply["session_id"])
|
|
79
|
+
|
|
80
|
+
# JSON-mode response, custom system prompt
|
|
81
|
+
client.chat.send("List 3 colors", response_format="json", system_prompt="Be terse")
|
|
82
|
+
|
|
83
|
+
# Sessions
|
|
84
|
+
client.chat.list_sessions() # -> [session_id, ...]
|
|
85
|
+
client.chat.get_history(session_id) # -> {"session_id", "history": [...]}
|
|
86
|
+
client.chat.clear_history(session_id)
|
|
87
|
+
|
|
88
|
+
# Summarize an uploaded file (path, or (filename, content[, content_type]))
|
|
89
|
+
client.chat.summarize_file("report.pdf")
|
|
90
|
+
client.chat.summarize_file(("notes.txt", "raw text here"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> **Note on streaming:** the server streams chat and RAG answers as
|
|
94
|
+
> `text/event-stream`, not JSON. This client buffers the full response and
|
|
95
|
+
> parses the leading marker lines (`[SESSION_ID:...]`, and for RAG
|
|
96
|
+
> `[SEARCH_QUERY:...]` / `[SOURCES:...]`) out of the body for you, so
|
|
97
|
+
> `chat.send` returns `{"session_id", "response", "response_format"}` and
|
|
98
|
+
> `rag.ask` returns `{"answer", "sources", "session_id", "search_query"}`.
|
|
99
|
+
> Buffering is the right default for scripts and backends; token-by-token
|
|
100
|
+
> iteration is not yet exposed.
|
|
101
|
+
|
|
102
|
+
## RAG
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Ingest one or many documents into a collection
|
|
106
|
+
client.rag.upload("manual.pdf", collection_name="docs")
|
|
107
|
+
client.rag.upload([("a.txt", "..."), ("b.txt", "...")], collection_name="docs")
|
|
108
|
+
|
|
109
|
+
# Ask a question grounded in a collection
|
|
110
|
+
ans = client.rag.ask("What does the manual say about setup?", collection_name="docs")
|
|
111
|
+
print(ans)
|
|
112
|
+
|
|
113
|
+
# Embeddings, listing, deletion, compare, summarize
|
|
114
|
+
client.rag.embed("some text")
|
|
115
|
+
client.rag.list_collections()
|
|
116
|
+
client.rag.list_files("docs")
|
|
117
|
+
client.rag.delete_file("docs", "a.txt")
|
|
118
|
+
client.rag.delete_collection("docs")
|
|
119
|
+
client.rag.compare("docs", "a.txt", "b.txt")
|
|
120
|
+
client.rag.summarize_document("docs", "manual.pdf")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Error handling
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from praixis import (
|
|
127
|
+
APIError, AuthenticationError, NotFoundError,
|
|
128
|
+
RateLimitError, APIConnectionError,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
client.chat.send("hi")
|
|
133
|
+
except AuthenticationError:
|
|
134
|
+
... # 401 / 403
|
|
135
|
+
except NotFoundError:
|
|
136
|
+
... # 404
|
|
137
|
+
except RateLimitError:
|
|
138
|
+
... # 429 (per-route limits)
|
|
139
|
+
except APIError as e:
|
|
140
|
+
print(e.status_code, e.detail)
|
|
141
|
+
except APIConnectionError:
|
|
142
|
+
... # never reached the server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
All exceptions inherit from `praixis.PraixisError`.
|
|
146
|
+
|
|
147
|
+
## Testing
|
|
148
|
+
|
|
149
|
+
Both suites run against a standard-library mock HTTP server — no network needed.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Sync client — zero dependencies
|
|
153
|
+
uv run --no-project python tests/test_client.py
|
|
154
|
+
|
|
155
|
+
# Async client — needs httpx
|
|
156
|
+
uv run --with httpx python tests/test_async_client.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The async suite also asserts that the sync and async resources expose an
|
|
160
|
+
identical set of methods, so the two clients can't silently drift apart.
|
|
161
|
+
|
|
162
|
+
## Privacy note
|
|
163
|
+
|
|
164
|
+
This client transmits whatever you pass to it (prompts, documents, session IDs)
|
|
165
|
+
to the configured Praixis Engine server. Those payloads may contain personal
|
|
166
|
+
data — handle them according to your own privacy obligations. The client stores
|
|
167
|
+
nothing locally and adds no telemetry.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT — see [LICENSE](./LICENSE).
|
praixis-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Praixis Engine — Python Client
|
|
2
|
+
|
|
3
|
+
A lightweight Python client for the Praixis Engine API, in both **sync** and
|
|
4
|
+
**async** flavors.
|
|
5
|
+
|
|
6
|
+
- **`PraixisClient`** — synchronous, **zero dependencies**, built entirely on
|
|
7
|
+
the standard library (`urllib`, `json`, `uuid`), so an upstream package
|
|
8
|
+
release can never break it.
|
|
9
|
+
- **`AsyncPraixisClient`** — async/await, built on `httpx` (the only optional
|
|
10
|
+
dependency). Imported lazily, so the sync client stays dependency-free.
|
|
11
|
+
- Same surface in both: resource-grouped `client.chat` and `client.rag` — chat
|
|
12
|
+
+ file summary, and RAG ingest/ask/embed/compare.
|
|
13
|
+
|
|
14
|
+
> The companion Node.js client lives in its own repository.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install praixis # sync client only, zero dependencies
|
|
20
|
+
pip install "praixis[async]" # also installs httpx for the async client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or vendor the `praixis/` package directly into your project — the sync client
|
|
24
|
+
has no deps.
|
|
25
|
+
|
|
26
|
+
Requires Python 3.10+.
|
|
27
|
+
|
|
28
|
+
## Authentication
|
|
29
|
+
|
|
30
|
+
Every endpoint uses an app-level API key sent as the `X-API-Key` header.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from praixis import PraixisClient
|
|
34
|
+
|
|
35
|
+
client = PraixisClient("http://localhost:8080", "your-api-key")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> Admin/system routes (`/api/system/*`, HTTP Basic auth) are intentionally **not**
|
|
39
|
+
> part of this SDK. They already have a browser UI, and baking admin credentials
|
|
40
|
+
> into application code would be a security anti-pattern. The `X-API-Key` this
|
|
41
|
+
> SDK uses is an app-level credential, not an admin one.
|
|
42
|
+
|
|
43
|
+
## Chat
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Start a conversation
|
|
47
|
+
reply = client.chat.send("Hello, world!")
|
|
48
|
+
print(reply["session_id"], reply["response"])
|
|
49
|
+
|
|
50
|
+
# Continue it
|
|
51
|
+
client.chat.send("And again?", session_id=reply["session_id"])
|
|
52
|
+
|
|
53
|
+
# JSON-mode response, custom system prompt
|
|
54
|
+
client.chat.send("List 3 colors", response_format="json", system_prompt="Be terse")
|
|
55
|
+
|
|
56
|
+
# Sessions
|
|
57
|
+
client.chat.list_sessions() # -> [session_id, ...]
|
|
58
|
+
client.chat.get_history(session_id) # -> {"session_id", "history": [...]}
|
|
59
|
+
client.chat.clear_history(session_id)
|
|
60
|
+
|
|
61
|
+
# Summarize an uploaded file (path, or (filename, content[, content_type]))
|
|
62
|
+
client.chat.summarize_file("report.pdf")
|
|
63
|
+
client.chat.summarize_file(("notes.txt", "raw text here"))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Note on streaming:** the server streams chat and RAG answers as
|
|
67
|
+
> `text/event-stream`, not JSON. This client buffers the full response and
|
|
68
|
+
> parses the leading marker lines (`[SESSION_ID:...]`, and for RAG
|
|
69
|
+
> `[SEARCH_QUERY:...]` / `[SOURCES:...]`) out of the body for you, so
|
|
70
|
+
> `chat.send` returns `{"session_id", "response", "response_format"}` and
|
|
71
|
+
> `rag.ask` returns `{"answer", "sources", "session_id", "search_query"}`.
|
|
72
|
+
> Buffering is the right default for scripts and backends; token-by-token
|
|
73
|
+
> iteration is not yet exposed.
|
|
74
|
+
|
|
75
|
+
## RAG
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Ingest one or many documents into a collection
|
|
79
|
+
client.rag.upload("manual.pdf", collection_name="docs")
|
|
80
|
+
client.rag.upload([("a.txt", "..."), ("b.txt", "...")], collection_name="docs")
|
|
81
|
+
|
|
82
|
+
# Ask a question grounded in a collection
|
|
83
|
+
ans = client.rag.ask("What does the manual say about setup?", collection_name="docs")
|
|
84
|
+
print(ans)
|
|
85
|
+
|
|
86
|
+
# Embeddings, listing, deletion, compare, summarize
|
|
87
|
+
client.rag.embed("some text")
|
|
88
|
+
client.rag.list_collections()
|
|
89
|
+
client.rag.list_files("docs")
|
|
90
|
+
client.rag.delete_file("docs", "a.txt")
|
|
91
|
+
client.rag.delete_collection("docs")
|
|
92
|
+
client.rag.compare("docs", "a.txt", "b.txt")
|
|
93
|
+
client.rag.summarize_document("docs", "manual.pdf")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Error handling
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from praixis import (
|
|
100
|
+
APIError, AuthenticationError, NotFoundError,
|
|
101
|
+
RateLimitError, APIConnectionError,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
client.chat.send("hi")
|
|
106
|
+
except AuthenticationError:
|
|
107
|
+
... # 401 / 403
|
|
108
|
+
except NotFoundError:
|
|
109
|
+
... # 404
|
|
110
|
+
except RateLimitError:
|
|
111
|
+
... # 429 (per-route limits)
|
|
112
|
+
except APIError as e:
|
|
113
|
+
print(e.status_code, e.detail)
|
|
114
|
+
except APIConnectionError:
|
|
115
|
+
... # never reached the server
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
All exceptions inherit from `praixis.PraixisError`.
|
|
119
|
+
|
|
120
|
+
## Testing
|
|
121
|
+
|
|
122
|
+
Both suites run against a standard-library mock HTTP server — no network needed.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Sync client — zero dependencies
|
|
126
|
+
uv run --no-project python tests/test_client.py
|
|
127
|
+
|
|
128
|
+
# Async client — needs httpx
|
|
129
|
+
uv run --with httpx python tests/test_async_client.py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The async suite also asserts that the sync and async resources expose an
|
|
133
|
+
identical set of methods, so the two clients can't silently drift apart.
|
|
134
|
+
|
|
135
|
+
## Privacy note
|
|
136
|
+
|
|
137
|
+
This client transmits whatever you pass to it (prompts, documents, session IDs)
|
|
138
|
+
to the configured Praixis Engine server. Those payloads may contain personal
|
|
139
|
+
data — handle them according to your own privacy obligations. The client stores
|
|
140
|
+
nothing locally and adds no telemetry.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Praixis Engine Python client.
|
|
2
|
+
|
|
3
|
+
The synchronous :class:`PraixisClient` is built entirely on the standard
|
|
4
|
+
library, so it has zero dependencies and upstream package releases can never
|
|
5
|
+
break it::
|
|
6
|
+
|
|
7
|
+
from praixis import PraixisClient
|
|
8
|
+
|
|
9
|
+
client = PraixisClient("http://localhost:8080", "your-api-key")
|
|
10
|
+
print(client.chat.send("Hello")["response"])
|
|
11
|
+
|
|
12
|
+
An :class:`AsyncPraixisClient` is also available for async/await code. It is
|
|
13
|
+
backed by httpx, the SDK's only (optional) dependency - install it with
|
|
14
|
+
``pip install praixis[async]``. The import is lazy, so httpx is required only if
|
|
15
|
+
you actually use the async client; ``import praixis`` alone never needs it::
|
|
16
|
+
|
|
17
|
+
from praixis import AsyncPraixisClient
|
|
18
|
+
|
|
19
|
+
async with AsyncPraixisClient("http://localhost:8080", "your-api-key") as client:
|
|
20
|
+
print((await client.chat.send("Hello"))["response"])
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from .client import PraixisClient
|
|
28
|
+
from .errors import (
|
|
29
|
+
APIConnectionError,
|
|
30
|
+
APIError,
|
|
31
|
+
AuthenticationError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
PraixisError,
|
|
34
|
+
RateLimitError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
# Give type checkers / IDEs the symbol without importing httpx at runtime.
|
|
39
|
+
from .aio import AsyncPraixisClient
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"PraixisClient",
|
|
45
|
+
"AsyncPraixisClient",
|
|
46
|
+
"PraixisError",
|
|
47
|
+
"APIError",
|
|
48
|
+
"APIConnectionError",
|
|
49
|
+
"AuthenticationError",
|
|
50
|
+
"NotFoundError",
|
|
51
|
+
"RateLimitError",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __getattr__(name: str) -> object:
|
|
57
|
+
# Lazily resolve the async client so importing praixis never pulls in httpx
|
|
58
|
+
# unless the async client is actually requested.
|
|
59
|
+
if name == "AsyncPraixisClient":
|
|
60
|
+
from .aio import AsyncPraixisClient
|
|
61
|
+
|
|
62
|
+
return AsyncPraixisClient
|
|
63
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Helpers for turning caller-supplied files into multipart parts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from ._transport import FilePart
|
|
9
|
+
|
|
10
|
+
# What a caller may hand us for a single file:
|
|
11
|
+
# * a path on disk (str)
|
|
12
|
+
# * a (filename, content) pair
|
|
13
|
+
# * a (filename, content, content_type) triple
|
|
14
|
+
FileInput = Union[str, tuple]
|
|
15
|
+
|
|
16
|
+
_DEFAULT_CONTENT_TYPE = "application/octet-stream"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_part(item: FileInput, field_name: str) -> FilePart:
|
|
20
|
+
"""Normalize one file input into a ``(field, filename, bytes, type)`` part."""
|
|
21
|
+
if isinstance(item, str):
|
|
22
|
+
with open(item, "rb") as fh:
|
|
23
|
+
content = fh.read()
|
|
24
|
+
return (field_name, os.path.basename(item), content, _DEFAULT_CONTENT_TYPE)
|
|
25
|
+
|
|
26
|
+
if isinstance(item, tuple):
|
|
27
|
+
if len(item) == 2:
|
|
28
|
+
filename, content = item
|
|
29
|
+
content_type = _DEFAULT_CONTENT_TYPE
|
|
30
|
+
elif len(item) == 3:
|
|
31
|
+
filename, content, content_type = item
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError("file tuple must be (filename, content) or (filename, content, content_type)")
|
|
34
|
+
if isinstance(content, str):
|
|
35
|
+
content = content.encode("utf-8")
|
|
36
|
+
return (field_name, filename, content, content_type)
|
|
37
|
+
|
|
38
|
+
raise TypeError(f"unsupported file input: {type(item)!r}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to_parts(items: FileInput | list[FileInput], field_name: str) -> list[FilePart]:
|
|
42
|
+
"""Normalize one file or a list of files into parts under ``field_name``."""
|
|
43
|
+
if isinstance(items, list):
|
|
44
|
+
return [to_part(it, field_name) for it in items]
|
|
45
|
+
return [to_part(items, field_name)]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""HTTP primitives shared by the sync and async transports.
|
|
2
|
+
|
|
3
|
+
Keeping these in one place means the auth scheme and the request/response
|
|
4
|
+
shapes are defined once and reused, so the two transports can never drift
|
|
5
|
+
apart.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def encode_path_segment(value: str) -> str:
|
|
16
|
+
"""Percent-encode a single URL path segment (filename, session id, ...).
|
|
17
|
+
|
|
18
|
+
Uses ``safe=""`` so reserved characters - spaces, ``/``, ``?``, ``#``, ``%``
|
|
19
|
+
- in a value like a filename can't corrupt the URL. Values already limited to
|
|
20
|
+
``[A-Za-z0-9_.-]`` (collection names, the usual ids) pass through unchanged.
|
|
21
|
+
"""
|
|
22
|
+
return quote(str(value), safe="")
|
|
23
|
+
|
|
24
|
+
# A single file part: (field_name, filename, content_bytes, content_type).
|
|
25
|
+
FilePart = tuple[str, str, bytes, str]
|
|
26
|
+
# A single form field: (field_name, value).
|
|
27
|
+
FormField = tuple[str, str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def auth_headers(api_key: str) -> dict[str, str]:
|
|
31
|
+
"""Build the auth headers for a request.
|
|
32
|
+
|
|
33
|
+
Every app endpoint (chat + RAG) authenticates with the app-level
|
|
34
|
+
``X-API-Key`` header. (Admin routes use HTTP Basic and a browser UI; they're
|
|
35
|
+
deliberately out of scope for this SDK.)
|
|
36
|
+
"""
|
|
37
|
+
return {"X-API-Key": api_key} if api_key else {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# A streamed (text/event-stream) line of the form ``[KEY:value]``. The server
|
|
41
|
+
# prefixes its streamed chat / RAG / summary responses with these marker lines
|
|
42
|
+
# (e.g. [SESSION_ID:...], [SEARCH_QUERY:...], [SOURCES:a,b], [FILE:...],
|
|
43
|
+
# [PROGRESS:...], [ERROR:...]) before emitting the generated text.
|
|
44
|
+
_MARKER_RE = re.compile(r"^\[([A-Z_]+):(.*)\]$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_event_stream(text: str) -> dict[str, Any]:
|
|
48
|
+
"""Parse a buffered ``text/event-stream`` body into markers + generated text.
|
|
49
|
+
|
|
50
|
+
The chat, RAG-ask and file-summary endpoints don't return JSON - they stream
|
|
51
|
+
plain text whose leading lines are ``[KEY:value]`` markers followed by the
|
|
52
|
+
model's output. Both transports buffer that body and hand it here so the two
|
|
53
|
+
clients shape streamed responses identically.
|
|
54
|
+
|
|
55
|
+
Returns a dict with the recognised markers (``session_id``, ``search_query``,
|
|
56
|
+
``sources`` as a list, ``file``), the joined generated ``text``, and the raw
|
|
57
|
+
``markers`` map for anything else (e.g. ``PROGRESS``/``ERROR``).
|
|
58
|
+
"""
|
|
59
|
+
markers: dict[str, str] = {}
|
|
60
|
+
body_lines: list[str] = []
|
|
61
|
+
for line in text.split("\n"):
|
|
62
|
+
m = _MARKER_RE.match(line)
|
|
63
|
+
if m:
|
|
64
|
+
markers[m.group(1)] = m.group(2)
|
|
65
|
+
else:
|
|
66
|
+
body_lines.append(line)
|
|
67
|
+
|
|
68
|
+
sources_raw = markers.get("SOURCES")
|
|
69
|
+
sources = [s for s in sources_raw.split(",") if s] if sources_raw is not None else None
|
|
70
|
+
return {
|
|
71
|
+
"session_id": markers.get("SESSION_ID"),
|
|
72
|
+
"search_query": markers.get("SEARCH_QUERY"),
|
|
73
|
+
"sources": sources,
|
|
74
|
+
"file": markers.get("FILE"),
|
|
75
|
+
"text": "\n".join(body_lines).strip("\n"),
|
|
76
|
+
"markers": markers,
|
|
77
|
+
}
|