x64acp 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.
Files changed (64) hide show
  1. x64acp-0.1.0/.github/workflows/publish.yml +21 -0
  2. x64acp-0.1.0/.gitignore +9 -0
  3. x64acp-0.1.0/CLAUDE.md +72 -0
  4. x64acp-0.1.0/LICENSE +21 -0
  5. x64acp-0.1.0/PKG-INFO +206 -0
  6. x64acp-0.1.0/README.md +184 -0
  7. x64acp-0.1.0/examples/fastapi_anthropic.py +120 -0
  8. x64acp-0.1.0/examples/fastapi_basic.py +30 -0
  9. x64acp-0.1.0/examples/fastapi_langchain.py +54 -0
  10. x64acp-0.1.0/examples/fastapi_langgraph.py +80 -0
  11. x64acp-0.1.0/examples/fastapi_multiple_endpoints.py +108 -0
  12. x64acp-0.1.0/examples/fastapi_openai.py +119 -0
  13. x64acp-0.1.0/examples/fastapi_openai_agents.py +83 -0
  14. x64acp-0.1.0/examples/fastapi_tools.py +70 -0
  15. x64acp-0.1.0/examples/fastapi_vertex.py +55 -0
  16. x64acp-0.1.0/examples/fastapi_whatsapp_webhook.py +114 -0
  17. x64acp-0.1.0/pyproject.toml +59 -0
  18. x64acp-0.1.0/src/x64acp/__init__.py +164 -0
  19. x64acp-0.1.0/src/x64acp/a2a/__init__.py +43 -0
  20. x64acp-0.1.0/src/x64acp/a2a/client.py +138 -0
  21. x64acp-0.1.0/src/x64acp/a2a/models.py +153 -0
  22. x64acp-0.1.0/src/x64acp/a2a/server.py +229 -0
  23. x64acp-0.1.0/src/x64acp/analytics/__init__.py +11 -0
  24. x64acp-0.1.0/src/x64acp/analytics/agent_analytics.py +108 -0
  25. x64acp-0.1.0/src/x64acp/analytics/collector.py +218 -0
  26. x64acp-0.1.0/src/x64acp/analytics/models.py +43 -0
  27. x64acp-0.1.0/src/x64acp/auth.py +32 -0
  28. x64acp-0.1.0/src/x64acp/broadcast.py +133 -0
  29. x64acp-0.1.0/src/x64acp/config.py +82 -0
  30. x64acp-0.1.0/src/x64acp/dispatch.py +51 -0
  31. x64acp-0.1.0/src/x64acp/exceptions.py +27 -0
  32. x64acp-0.1.0/src/x64acp/handlers/__init__.py +22 -0
  33. x64acp-0.1.0/src/x64acp/handlers/context.py +20 -0
  34. x64acp-0.1.0/src/x64acp/handlers/incoming.py +29 -0
  35. x64acp-0.1.0/src/x64acp/handlers/knowledge.py +29 -0
  36. x64acp-0.1.0/src/x64acp/handlers/memory.py +123 -0
  37. x64acp-0.1.0/src/x64acp/handlers/pipeline.py +1149 -0
  38. x64acp-0.1.0/src/x64acp/handlers/router.py +1297 -0
  39. x64acp-0.1.0/src/x64acp/handlers/send.py +355 -0
  40. x64acp-0.1.0/src/x64acp/jsonrpc.py +100 -0
  41. x64acp-0.1.0/src/x64acp/mcp/__init__.py +22 -0
  42. x64acp-0.1.0/src/x64acp/mcp/client.py +129 -0
  43. x64acp-0.1.0/src/x64acp/mcp/models.py +64 -0
  44. x64acp-0.1.0/src/x64acp/migrations/__init__.py +3 -0
  45. x64acp-0.1.0/src/x64acp/migrations/schemas.py +129 -0
  46. x64acp-0.1.0/src/x64acp/persistence/__init__.py +9 -0
  47. x64acp-0.1.0/src/x64acp/persistence/base.py +81 -0
  48. x64acp-0.1.0/src/x64acp/persistence/cosmos.py +725 -0
  49. x64acp-0.1.0/src/x64acp/persistence/postgres.py +890 -0
  50. x64acp-0.1.0/src/x64acp/persistence/upload.py +70 -0
  51. x64acp-0.1.0/src/x64acp/protocol/__init__.py +104 -0
  52. x64acp-0.1.0/src/x64acp/protocol/frames.py +538 -0
  53. x64acp-0.1.0/src/x64acp/protocol/models.py +306 -0
  54. x64acp-0.1.0/src/x64acp/protocol/parser.py +21 -0
  55. x64acp-0.1.0/src/x64acp/py.typed +0 -0
  56. x64acp-0.1.0/src/x64acp/registry.py +79 -0
  57. x64acp-0.1.0/src/x64acp/repository.py +181 -0
  58. x64acp-0.1.0/src/x64acp/server.py +365 -0
  59. x64acp-0.1.0/src/x64acp/settings/__init__.py +21 -0
  60. x64acp-0.1.0/src/x64acp/settings/agent_settings.py +68 -0
  61. x64acp-0.1.0/src/x64acp/settings/broadcaster.py +128 -0
  62. x64acp-0.1.0/src/x64acp/settings/models.py +167 -0
  63. x64acp-0.1.0/src/x64acp/variables.py +33 -0
  64. x64acp-0.1.0/uv.lock +728 -0
@@ -0,0 +1,21 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11"
19
+ - run: pip install build
20
+ - run: python -m build
21
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ .venv
2
+ venv
3
+ __pycache__
4
+ .claude
5
+ .ruff_cache
6
+ .pytest_cache
7
+ .mypy_cache
8
+ /docs/plans
9
+ dist
x64acp-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ x64acp is a Python SDK for building AI assistant servers. It provides WebSocket-based real-time communication, persistence (PostgreSQL/Cosmos DB), analytics, settings management, A2A (Agent-to-Agent) protocol support, and MCP client integration.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ uv sync --all-extras # Install all dependencies
13
+ uv run poe test # Run tests
14
+ uv run poe test:cov # Tests with coverage
15
+ uv run poe check # Lint (ruff)
16
+ uv run poe check:fix # Auto-fix lint issues
17
+ uv run poe format # Format code
18
+ uv run poe format:imports # Sort imports
19
+ uv run poe typecheck # Type check with mypy
20
+ uv run poe pcu # Check package updates
21
+ uv run poe pcu:upgrade # Upgrade packages
22
+ ```
23
+
24
+ Run a single test: `uv run pytest tests/path/to/test.py::test_name -v`
25
+
26
+ ## Architecture
27
+
28
+ ### Core Flow
29
+
30
+ `AssistantServer` is the entry point. It has two modes of operation:
31
+
32
+ 1. **WebSocket mode** (`server.handle(websocket, handler)`) — Long-lived connection. The `Router` parses incoming frames (`ClientToServer`) and dispatches them. When a user sends a message (`C2S_EventCreate`), the `AssistantHandlerPipeline` orchestrates thread/event/run creation, invokes the user-provided `AssistantHandler`, persists results, and broadcasts `ServerToClient` frames back.
33
+
34
+ 2. **Dispatch mode** (`server.dispatch(handler, ...)`) — Programmatic invocation without WebSocket. Returns a `DispatchStream` (async iterable of `AssistantEvent`) with `thread_id`, `event_id`, and `is_new_thread` available immediately before iteration.
35
+
36
+ ### Assistant Handler Contract
37
+
38
+ AssistantHandlers are async generators with signature:
39
+ ```python
40
+ async def handler(incoming, send, memory, analytics, settings) -> AsyncGenerator[AssistantEvent, None]:
41
+ yield send.text("response")
42
+ ```
43
+
44
+ The five injected parameters (`AssistantIncoming`, `AssistantSend`, `AssistantMemory`, `AssistantAnalytics`, `AssistantSettings`) are constructed by `AssistantHandlerPipeline` per request.
45
+
46
+ ### Protocol Layer (`protocol/`)
47
+
48
+ - `frames.py` — Pydantic models for all WebSocket frame types (C2S_* and S2C_*), discriminated by `type` field
49
+ - `models.py` — Domain models: `Thread`, `ThreadEvent`, `Run`, content parts (internal vs wire C2S/S2C variants)
50
+ - `parser.py` — JSON frame parsing/serialization using Pydantic `TypeAdapter`
51
+
52
+ ### Persistence (`persistence/`)
53
+
54
+ `Persistence` is a Protocol (structural typing) with two implementations: `PostgresPersistence` (asyncpg + SQLAlchemy) and `CosmosPersistence`. Both handle threads, events, runs, settings, and analytics storage. Cosmos is lazily imported only when `database_type="cosmos"`.
55
+
56
+ ### Subsystems
57
+
58
+ - **`broadcast.py`** — `BroadcastManager` pushes events/thread updates to subscribed WebSocket connections via `ConnectionRegistry`
59
+ - **`registry.py`** — `ConnectionRegistry` maps WebSocket connections to assistant instance IDs
60
+ - **`analytics/`** — `AnalyticsCollector` batches events and flushes to persistence + optional webhook; `AssistantAnalytics` is the handler-facing API
61
+ - **`settings/`** — Schema-driven settings with `SettingsBroadcaster` for real-time sync to subscribed clients
62
+ - **`a2a/`** — Agent-to-Agent protocol: `A2AServer` (FastAPI router for JSON-RPC) and `A2AClient`
63
+ - **`mcp/`** — `MCPClient` connects to MCP-compliant tool servers over Streamable HTTP
64
+ - **`handlers/knowledge.py`** — `AssistantKnowledge` for knowledge source selection by clients
65
+
66
+ ## Code Style
67
+
68
+ - Python 3.11+, ruff for linting/formatting, line length 120
69
+ - Quote style: double quotes
70
+ - Ruff rules: E, F, I (isort), UP (pyupgrade)
71
+ - `pytest-asyncio` with `asyncio_mode = "auto"` (no need for `@pytest.mark.asyncio`)
72
+ - Pydantic v2 for all models; dataclasses for config objects
x64acp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 0x0064 (github.com/0x0064)
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.
x64acp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: x64acp
3
+ Version: 0.1.0
4
+ Summary: Agent Communication Protocol (ACP) Python SDK
5
+ Project-URL: Repository, https://github.com/0x0064/x64acp-server
6
+ Author-email: 0x0064 <user.frndvrgs@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: aiosqlite>=0.22.1
11
+ Requires-Dist: asyncpg>=0.31.0
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: sqlalchemy>=2.0.48
15
+ Provides-Extra: dev
16
+ Requires-Dist: mypy>=1.19.1; extra == 'dev'
17
+ Requires-Dist: poethepoet>=0.42.1; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
19
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
20
+ Requires-Dist: ruff>=0.15.7; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <img width="200" alt="x64acp-ember" src="https://github.com/user-attachments/assets/da0940dc-7270-4a52-b969-9d5b6d5fa0ea" />
24
+
25
+ Agent Communication Protocol (ACP) Python SDK
26
+
27
+ [Documentation](docs/) · [Examples](examples/)
28
+
29
+ ```python
30
+ from x64acp import AssistantServer, AssistantServerConfig, AssistantSend, AssistantIncoming
31
+
32
+ server = AssistantServer(config=AssistantServerConfig(
33
+ database_type="postgres",
34
+ database_host="localhost",
35
+ database_name="myapp",
36
+ ))
37
+
38
+ async def my_handler(incoming: AssistantIncoming, send: AssistantSend, memory, analytics, settings, knowledge):
39
+ history = await memory.get_history(limit=20, format="openai")
40
+ yield send.run_start()
41
+ yield send.text("Hello! How can I help?")
42
+ yield send.run_end()
43
+
44
+ # FastAPI
45
+ @app.websocket("/ws")
46
+ async def ws(websocket: WebSocket):
47
+ await websocket.accept()
48
+ await server.handle(websocket, my_handler)
49
+
50
+ # Programmatic (HTTP, jobs, webhooks)
51
+ stream = await server.dispatch(my_handler, assistant_id="my-assistant", message="Hi")
52
+ async for event in stream:
53
+ print(event)
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ uv add x64acp # add to your project
60
+
61
+ uv sync --all-extras # setup with all optional
62
+ uv run poe check # ruff lint
63
+ uv run poe check:fix # ruff lint + auto-fix
64
+ uv run poe format # ruff format
65
+ uv run poe format:imports # sort imports
66
+ uv run poe typecheck # mypy type checking
67
+ uv run poe test # pytest
68
+ ```
69
+
70
+ Private package — installed as editable dependency:
71
+
72
+ ```toml
73
+ # In consumer pyproject.toml:
74
+ x64acp = { path = "../packages/x64acp", editable = true }
75
+ ```
76
+
77
+ ## Persistence
78
+
79
+ | Backend | Config | Requires |
80
+ |---------|--------|----------|
81
+ | PostgreSQL | `database_type="postgres"` | `asyncpg` |
82
+ | Azure Cosmos DB | `database_type="cosmos"` | `azure-cosmos` |
83
+
84
+ Auto-migrates on startup (`database_auto_migrate=True`). Both backends implement the same `Persistence` protocol.
85
+
86
+ ## Authentication & Authorization
87
+
88
+ ```python
89
+ class MyAuth:
90
+ async def authenticate(self, token: str, user: UserIdentity | None) -> UserIdentity | None:
91
+ ... # validate JWT, return user or None to reject
92
+
93
+ class MyAuthz:
94
+ async def authorize(self, user, tenant, action, resource) -> AuthorizationResult | None:
95
+ ... # check permissions
96
+
97
+ server = AssistantServer(config=AssistantServerConfig(
98
+ authentication=MyAuth(),
99
+ authorization=MyAuthz(),
100
+ ...
101
+ ))
102
+ ```
103
+
104
+ - `Authentication` — called once per WebSocket connection, validated identity overrides client-sent fields
105
+ - `Authorization` — called per-action for fine-grained access control
106
+ - Both optional — when `None`, all connections and actions are accepted
107
+
108
+ ## Identity & Scoping
109
+
110
+ All identity models use `extra="allow"` — attach any fields your application needs.
111
+
112
+ | Model | Base Fields | Purpose |
113
+ |-------|-------------|---------|
114
+ | `UserIdentity` | `id` | Who sent the message |
115
+ | `AssistantIdentity` | `id`, `name` | Which assistant responded |
116
+ | `SystemContext` | `id` | System-level metadata (environment, version) |
117
+ | `TenantScope` | *(none)* | Organization/workspace scoping |
118
+
119
+ Thread scoping uses JSONB containment on `owner_tenant` and `owner_user` — the consumer controls scope granularity by what they put in `tenant` and `user` at connection time.
120
+
121
+ ## Handler API
122
+
123
+ ```python
124
+ async def handler(
125
+ incoming: AssistantIncoming, # message, content, context
126
+ send: AssistantSend, # yield events back to client
127
+ memory: AssistantMemory, # conversation history
128
+ analytics: AssistantAnalytics, # track custom events
129
+ settings: AssistantSettings, # runtime settings
130
+ knowledge: AssistantKnowledge, # knowledge source selection
131
+ ) -> AsyncGenerator[AssistantEvent, None]:
132
+ yield send.text("response")
133
+ yield send.tool_transaction("search", {"q": "test"}, {"results": [...]})
134
+ yield send.analysis("sentiment", {"score": 0.8}, text="Positive sentiment")
135
+ ```
136
+
137
+ ### Send Methods
138
+
139
+ | Method | Description |
140
+ |--------|-------------|
141
+ | `text(text)` | Text message |
142
+ | `image(url, mime_type?, alt?)` | Image |
143
+ | `document(url, filename, mime_type)` | Document |
144
+ | `audio(url, mime_type, duration?)` | Audio |
145
+ | `tool_call(name, args, display_name?, id?)` | Tool invocation |
146
+ | `tool_result(name, result, args?, display_name?, id?)` | Tool output |
147
+ | `tool_transaction(name, args, result, display_name?, id?)` | Combined call + result |
148
+ | `run_start(blueprint_id?)` | Start a run |
149
+ | `run_end(status?, error?)` | End a run |
150
+ | `confirmation(confirmation_id, action_type, blocking, summary, details?)` | Request user confirmation |
151
+ | `analysis(kind, metadata, *, text?)` | Structured analysis event |
152
+ | `mention(assistant_id, text)` | Cross-assistant mention |
153
+ | `skill_invoke(assistant_id, command, args?)` | Invoke skill on another assistant |
154
+ | `update_thread(*, title?, metadata?)` | Update thread title/metadata |
155
+ | `update_user_event(event_id, content)` | Update existing event content |
156
+
157
+ ### Memory
158
+
159
+ ```python
160
+ history = await memory.get_history(limit=20, format="openai")
161
+ # Formats: "openai", "anthropic", "google", "cohere", "voyage"
162
+
163
+ raw_events = await memory.get_history_raw(limit=50, order="asc")
164
+ ```
165
+
166
+ ### Settings
167
+
168
+ ```python
169
+ from x64acp.settings import SettingsSchema, SettingsScope, SettingsField, SettingsFieldType
170
+
171
+ schema = SettingsSchema(scopes=[
172
+ SettingsScope(name="admin_assistant", visibility="admin", fields=[...]),
173
+ SettingsScope(name="user_thread", visibility="user", fields=[...]),
174
+ ])
175
+
176
+ # In handler:
177
+ value = await settings.resolve("mode", scopes=[("user_thread", thread_id), ("admin_assistant", "")])
178
+ ```
179
+
180
+ Settings stored per `(assistant_id, scope, scope_id)`. Field types: `text`, `number`, `select`, `checkbox`, `textarea`, `password`, `email`, `date`, `image`.
181
+
182
+ ## Server API
183
+
184
+ | Method | Returns | Description |
185
+ |--------|---------|-------------|
186
+ | `handle(ws, handler)` | `None` | WebSocket loop (blocks until disconnect) |
187
+ | `dispatch(handler, assistant_id, message, **kwargs)` | `DispatchStream` | Programmatic handler execution |
188
+ | `repository()` | `Repository` | CRUD for threads/events/runs |
189
+ | `broadcast` | `BroadcastManager` | Real-time event broadcasting |
190
+ | `memory(thread_id, assistant_id)` | `AssistantMemory` | Conversation history |
191
+ | `settings(assistant_id)` | `AssistantSettings` | Runtime settings |
192
+ | `analytics(assistant_id, **kwargs)` | `AssistantAnalytics` | Analytics tracking |
193
+ | `upload(data, filename, mime_type)` | `str` | Stage file upload (returns `ref_id`) |
194
+ | `close()` | `None` | Shutdown and release resources |
195
+
196
+ ## Env Variables
197
+
198
+ ```bash
199
+ assistant_DATABASE_HOST=localhost
200
+ assistant_DATABASE_PORT=5432
201
+ assistant_DATABASE_USER=postgres
202
+ assistant_DATABASE_NAME=
203
+ assistant_DATABASE_PASSWORD=
204
+ assistant_DATABASE_KEY= # Cosmos DB only
205
+ assistant_DATABASE_SSL=false # false, true, require, verify-ca, verify-full
206
+ ```
x64acp-0.1.0/README.md ADDED
@@ -0,0 +1,184 @@
1
+ <img width="200" alt="x64acp-ember" src="https://github.com/user-attachments/assets/da0940dc-7270-4a52-b969-9d5b6d5fa0ea" />
2
+
3
+ Agent Communication Protocol (ACP) Python SDK
4
+
5
+ [Documentation](docs/) · [Examples](examples/)
6
+
7
+ ```python
8
+ from x64acp import AssistantServer, AssistantServerConfig, AssistantSend, AssistantIncoming
9
+
10
+ server = AssistantServer(config=AssistantServerConfig(
11
+ database_type="postgres",
12
+ database_host="localhost",
13
+ database_name="myapp",
14
+ ))
15
+
16
+ async def my_handler(incoming: AssistantIncoming, send: AssistantSend, memory, analytics, settings, knowledge):
17
+ history = await memory.get_history(limit=20, format="openai")
18
+ yield send.run_start()
19
+ yield send.text("Hello! How can I help?")
20
+ yield send.run_end()
21
+
22
+ # FastAPI
23
+ @app.websocket("/ws")
24
+ async def ws(websocket: WebSocket):
25
+ await websocket.accept()
26
+ await server.handle(websocket, my_handler)
27
+
28
+ # Programmatic (HTTP, jobs, webhooks)
29
+ stream = await server.dispatch(my_handler, assistant_id="my-assistant", message="Hi")
30
+ async for event in stream:
31
+ print(event)
32
+ ```
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ uv add x64acp # add to your project
38
+
39
+ uv sync --all-extras # setup with all optional
40
+ uv run poe check # ruff lint
41
+ uv run poe check:fix # ruff lint + auto-fix
42
+ uv run poe format # ruff format
43
+ uv run poe format:imports # sort imports
44
+ uv run poe typecheck # mypy type checking
45
+ uv run poe test # pytest
46
+ ```
47
+
48
+ Private package — installed as editable dependency:
49
+
50
+ ```toml
51
+ # In consumer pyproject.toml:
52
+ x64acp = { path = "../packages/x64acp", editable = true }
53
+ ```
54
+
55
+ ## Persistence
56
+
57
+ | Backend | Config | Requires |
58
+ |---------|--------|----------|
59
+ | PostgreSQL | `database_type="postgres"` | `asyncpg` |
60
+ | Azure Cosmos DB | `database_type="cosmos"` | `azure-cosmos` |
61
+
62
+ Auto-migrates on startup (`database_auto_migrate=True`). Both backends implement the same `Persistence` protocol.
63
+
64
+ ## Authentication & Authorization
65
+
66
+ ```python
67
+ class MyAuth:
68
+ async def authenticate(self, token: str, user: UserIdentity | None) -> UserIdentity | None:
69
+ ... # validate JWT, return user or None to reject
70
+
71
+ class MyAuthz:
72
+ async def authorize(self, user, tenant, action, resource) -> AuthorizationResult | None:
73
+ ... # check permissions
74
+
75
+ server = AssistantServer(config=AssistantServerConfig(
76
+ authentication=MyAuth(),
77
+ authorization=MyAuthz(),
78
+ ...
79
+ ))
80
+ ```
81
+
82
+ - `Authentication` — called once per WebSocket connection, validated identity overrides client-sent fields
83
+ - `Authorization` — called per-action for fine-grained access control
84
+ - Both optional — when `None`, all connections and actions are accepted
85
+
86
+ ## Identity & Scoping
87
+
88
+ All identity models use `extra="allow"` — attach any fields your application needs.
89
+
90
+ | Model | Base Fields | Purpose |
91
+ |-------|-------------|---------|
92
+ | `UserIdentity` | `id` | Who sent the message |
93
+ | `AssistantIdentity` | `id`, `name` | Which assistant responded |
94
+ | `SystemContext` | `id` | System-level metadata (environment, version) |
95
+ | `TenantScope` | *(none)* | Organization/workspace scoping |
96
+
97
+ Thread scoping uses JSONB containment on `owner_tenant` and `owner_user` — the consumer controls scope granularity by what they put in `tenant` and `user` at connection time.
98
+
99
+ ## Handler API
100
+
101
+ ```python
102
+ async def handler(
103
+ incoming: AssistantIncoming, # message, content, context
104
+ send: AssistantSend, # yield events back to client
105
+ memory: AssistantMemory, # conversation history
106
+ analytics: AssistantAnalytics, # track custom events
107
+ settings: AssistantSettings, # runtime settings
108
+ knowledge: AssistantKnowledge, # knowledge source selection
109
+ ) -> AsyncGenerator[AssistantEvent, None]:
110
+ yield send.text("response")
111
+ yield send.tool_transaction("search", {"q": "test"}, {"results": [...]})
112
+ yield send.analysis("sentiment", {"score": 0.8}, text="Positive sentiment")
113
+ ```
114
+
115
+ ### Send Methods
116
+
117
+ | Method | Description |
118
+ |--------|-------------|
119
+ | `text(text)` | Text message |
120
+ | `image(url, mime_type?, alt?)` | Image |
121
+ | `document(url, filename, mime_type)` | Document |
122
+ | `audio(url, mime_type, duration?)` | Audio |
123
+ | `tool_call(name, args, display_name?, id?)` | Tool invocation |
124
+ | `tool_result(name, result, args?, display_name?, id?)` | Tool output |
125
+ | `tool_transaction(name, args, result, display_name?, id?)` | Combined call + result |
126
+ | `run_start(blueprint_id?)` | Start a run |
127
+ | `run_end(status?, error?)` | End a run |
128
+ | `confirmation(confirmation_id, action_type, blocking, summary, details?)` | Request user confirmation |
129
+ | `analysis(kind, metadata, *, text?)` | Structured analysis event |
130
+ | `mention(assistant_id, text)` | Cross-assistant mention |
131
+ | `skill_invoke(assistant_id, command, args?)` | Invoke skill on another assistant |
132
+ | `update_thread(*, title?, metadata?)` | Update thread title/metadata |
133
+ | `update_user_event(event_id, content)` | Update existing event content |
134
+
135
+ ### Memory
136
+
137
+ ```python
138
+ history = await memory.get_history(limit=20, format="openai")
139
+ # Formats: "openai", "anthropic", "google", "cohere", "voyage"
140
+
141
+ raw_events = await memory.get_history_raw(limit=50, order="asc")
142
+ ```
143
+
144
+ ### Settings
145
+
146
+ ```python
147
+ from x64acp.settings import SettingsSchema, SettingsScope, SettingsField, SettingsFieldType
148
+
149
+ schema = SettingsSchema(scopes=[
150
+ SettingsScope(name="admin_assistant", visibility="admin", fields=[...]),
151
+ SettingsScope(name="user_thread", visibility="user", fields=[...]),
152
+ ])
153
+
154
+ # In handler:
155
+ value = await settings.resolve("mode", scopes=[("user_thread", thread_id), ("admin_assistant", "")])
156
+ ```
157
+
158
+ Settings stored per `(assistant_id, scope, scope_id)`. Field types: `text`, `number`, `select`, `checkbox`, `textarea`, `password`, `email`, `date`, `image`.
159
+
160
+ ## Server API
161
+
162
+ | Method | Returns | Description |
163
+ |--------|---------|-------------|
164
+ | `handle(ws, handler)` | `None` | WebSocket loop (blocks until disconnect) |
165
+ | `dispatch(handler, assistant_id, message, **kwargs)` | `DispatchStream` | Programmatic handler execution |
166
+ | `repository()` | `Repository` | CRUD for threads/events/runs |
167
+ | `broadcast` | `BroadcastManager` | Real-time event broadcasting |
168
+ | `memory(thread_id, assistant_id)` | `AssistantMemory` | Conversation history |
169
+ | `settings(assistant_id)` | `AssistantSettings` | Runtime settings |
170
+ | `analytics(assistant_id, **kwargs)` | `AssistantAnalytics` | Analytics tracking |
171
+ | `upload(data, filename, mime_type)` | `str` | Stage file upload (returns `ref_id`) |
172
+ | `close()` | `None` | Shutdown and release resources |
173
+
174
+ ## Env Variables
175
+
176
+ ```bash
177
+ assistant_DATABASE_HOST=localhost
178
+ assistant_DATABASE_PORT=5432
179
+ assistant_DATABASE_USER=postgres
180
+ assistant_DATABASE_NAME=
181
+ assistant_DATABASE_PASSWORD=
182
+ assistant_DATABASE_KEY= # Cosmos DB only
183
+ assistant_DATABASE_SSL=false # false, true, require, verify-ca, verify-full
184
+ ```
@@ -0,0 +1,120 @@
1
+ import os
2
+
3
+ from anthropic import AsyncAnthropic
4
+ from fastapi import FastAPI, WebSocket
5
+
6
+ from x64acp import (
7
+ AssistantServer,
8
+ AssistantServerConfig,
9
+ SettingsField,
10
+ SettingsFieldType,
11
+ SettingsSchema,
12
+ SettingsScope,
13
+ SettingsSelectOption,
14
+ )
15
+
16
+ app = FastAPI()
17
+
18
+ settings_schema = SettingsSchema(
19
+ scopes=[
20
+ SettingsScope(
21
+ name="admin_assistant",
22
+ visibility="admin",
23
+ fields=[
24
+ SettingsField(
25
+ id="model",
26
+ type=SettingsFieldType.SELECT,
27
+ label="Model",
28
+ required=True,
29
+ default="claude-sonnet-4-20250514",
30
+ options=[
31
+ SettingsSelectOption(value="claude-sonnet-4-20250514", label="Claude Sonnet 4"),
32
+ SettingsSelectOption(value="claude-opus-4-20250514", label="Claude Opus 4"),
33
+ SettingsSelectOption(value="claude-haiku-4-20250514", label="Claude Haiku 4"),
34
+ ],
35
+ ),
36
+ SettingsField(
37
+ id="max_tokens",
38
+ type=SettingsFieldType.NUMBER,
39
+ label="Max Tokens",
40
+ required=True,
41
+ default=1024,
42
+ min=1,
43
+ max=4096,
44
+ ),
45
+ ],
46
+ ),
47
+ SettingsScope(
48
+ name="user_thread",
49
+ visibility="user",
50
+ fields=[
51
+ SettingsField(
52
+ id="system_prompt",
53
+ type=SettingsFieldType.TEXTAREA,
54
+ label="System Prompt",
55
+ required=True,
56
+ default="You are a helpful assistant.",
57
+ rows=4,
58
+ ),
59
+ ],
60
+ ),
61
+ ]
62
+ )
63
+
64
+ assistant_server = AssistantServer(
65
+ AssistantServerConfig(
66
+ database_type="postgres",
67
+ database_name="assistant",
68
+ settings_schema=settings_schema,
69
+ )
70
+ )
71
+ client = AsyncAnthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
72
+
73
+
74
+ async def anthropic_assistant(incoming, send, memory, analytics, settings, knowledge):
75
+ yield send.run_start()
76
+
77
+ model = await settings.get("model", scope="admin_assistant")
78
+ max_tokens = await settings.get("max_tokens", scope="admin_assistant")
79
+ system_prompt = await settings.resolve(
80
+ "system_prompt",
81
+ scopes=[("user_thread", incoming.context.thread_id), ("admin_assistant", "")],
82
+ )
83
+
84
+ await analytics.track("llm.request", data={"model": model})
85
+
86
+ history = await memory.get_history(limit=20, format="anthropic")
87
+
88
+ messages = list(history)
89
+ messages.append({"role": "user", "content": incoming.message})
90
+
91
+ result = await client.messages.create(
92
+ model=model,
93
+ max_tokens=int(max_tokens),
94
+ system=system_prompt,
95
+ messages=messages,
96
+ )
97
+
98
+ await analytics.track(
99
+ "llm.response",
100
+ data={
101
+ "model": model,
102
+ "input_tokens": result.usage.input_tokens,
103
+ "output_tokens": result.usage.output_tokens,
104
+ },
105
+ )
106
+
107
+ yield send.text(result.content[0].text)
108
+ yield send.update_thread(title=incoming.message[:60])
109
+ yield send.run_end()
110
+
111
+
112
+ @app.websocket("/ws")
113
+ async def websocket_endpoint(websocket: WebSocket):
114
+ await websocket.accept()
115
+ await assistant_server.handle(websocket, anthropic_assistant)
116
+
117
+
118
+ @app.on_event("shutdown")
119
+ async def shutdown():
120
+ await assistant_server.close()
@@ -0,0 +1,30 @@
1
+ from fastapi import FastAPI, WebSocket
2
+
3
+ from x64acp import AssistantServer, AssistantServerConfig
4
+
5
+ app = FastAPI()
6
+ assistant_server = AssistantServer(
7
+ AssistantServerConfig(
8
+ database_type="postgres",
9
+ database_name="assistant",
10
+ assistant_name="Echo Bot",
11
+ )
12
+ )
13
+
14
+
15
+ async def echo_assistant(incoming, send, memory, analytics, settings, knowledge):
16
+ yield send.run_start()
17
+ yield send.text(f"You said: {incoming.message}")
18
+ yield send.update_thread(title=incoming.message[:60])
19
+ yield send.run_end()
20
+
21
+
22
+ @app.websocket("/ws")
23
+ async def websocket_endpoint(websocket: WebSocket):
24
+ await websocket.accept()
25
+ await assistant_server.handle(websocket, echo_assistant)
26
+
27
+
28
+ @app.on_event("shutdown")
29
+ async def shutdown():
30
+ await assistant_server.close()