diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0185f06c..8a899fe0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,10 +21,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
@@ -43,10 +43,10 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
@@ -61,7 +61,7 @@ jobs:
github.repository == 'stainless-sdks/hyperspell-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -81,10 +81,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 585c39f2..ea7fb3fd 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -17,10 +17,10 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.9.13'
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index b7b5ce04..fe86cc65 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'hyperspell/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 51acdaa4..8ea07c9a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.37.0"
+ ".": "0.38.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 60ac02f6..d0d7b46e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-6d6dbb68dd9021348431b28e08378d086b3eaf5e65b3dfa03125b1fdec417fa6.yml
-openapi_spec_hash: 6ad2b84ac07c482fe838929694e49015
-config_hash: bd8505e17db740d82e578d0edaa9bfe0
+configured_endpoints: 31
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-e57add0181eb2a057f8416eaf4020dd5b3042431342a51e3d4dc39af4a41aced.yml
+openapi_spec_hash: d0d66b814ebe56ac7c0135f9f3aab616
+config_hash: 11e84d884a86d2db0411c35fae6e9121
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86d769ac..3cff83dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,53 @@
# Changelog
+## 0.38.0 (2026-06-18)
+
+Full Changelog: [v0.37.0...v0.38.0](https://github.com/hyperspell/python-sdk/compare/v0.37.0...v0.38.0)
+
+### Features
+
+* **api:** api update ([6392fa7](https://github.com/hyperspell/python-sdk/commit/6392fa73061b1cfaf4efffd86653e416a3f132ce))
+* **api:** api update ([29f46b0](https://github.com/hyperspell/python-sdk/commit/29f46b0abc638850c7ea2ed800b886d8a81c3569))
+* **api:** api update ([22cdd86](https://github.com/hyperspell/python-sdk/commit/22cdd8677c2d30fad4ebc6fe97715fcf01628506))
+* **api:** api update ([eeb82b7](https://github.com/hyperspell/python-sdk/commit/eeb82b767927c783df8233b637e63f7c4c14640c))
+* **api:** api update ([02ef637](https://github.com/hyperspell/python-sdk/commit/02ef6374b47293a4b3cd5c5f1bd179a80973aac2))
+* **api:** api update ([e991d68](https://github.com/hyperspell/python-sdk/commit/e991d68d0bd84df2fc52804c431a03127374d1bb))
+* **api:** api update ([a95dee2](https://github.com/hyperspell/python-sdk/commit/a95dee2cb72e8c64be1a4523d7325848c6d9e227))
+* **api:** api update ([e160f73](https://github.com/hyperspell/python-sdk/commit/e160f73c629eff27681b2d13d8cdd9c645cad17e))
+* **api:** api update ([58c8d7c](https://github.com/hyperspell/python-sdk/commit/58c8d7cd0aeb73b2a28dcc30dadac3b5d63bdcb8))
+* **api:** api update ([42d3c82](https://github.com/hyperspell/python-sdk/commit/42d3c825a891a655d999e55ab02572c8cf7d283b))
+* **api:** api update ([edd9b41](https://github.com/hyperspell/python-sdk/commit/edd9b41ea428964bb74381e415df2e3616c6a46e))
+* **api:** api update ([cc26c43](https://github.com/hyperspell/python-sdk/commit/cc26c43e007a42b9ac4de6554d0280ef56b2b713))
+* **api:** api update ([aa0cc75](https://github.com/hyperspell/python-sdk/commit/aa0cc7576de289e8e8dd152c773339e9ac27aafe))
+* **api:** api update ([335d028](https://github.com/hyperspell/python-sdk/commit/335d0288f43e7f681a783f887ce1415ee57cad22))
+* **api:** api update ([3b25c10](https://github.com/hyperspell/python-sdk/commit/3b25c10a9f2be985dcfbfaf059f5595ea316d5f5))
+* **api:** api update ([92fd43a](https://github.com/hyperspell/python-sdk/commit/92fd43a735c10a678fb2e9b3f052c43607b6fb8a))
+* **api:** api update ([7a8e495](https://github.com/hyperspell/python-sdk/commit/7a8e495ee6b8f162dce5c3124b007d2d60f6cdd6))
+* **api:** manual updates ([b25c195](https://github.com/hyperspell/python-sdk/commit/b25c1958b809f7957f354ce2b44cf3a84b8b01e0))
+* **api:** manual updates ([5e31b8a](https://github.com/hyperspell/python-sdk/commit/5e31b8a01c916a8e9afaf744f7bb8e670c8692c4))
+* **api:** manual updates ([cb78a3e](https://github.com/hyperspell/python-sdk/commit/cb78a3e76e04a5ead28e3ea0d5433d05cc6a64d1))
+* **api:** manual updates ([23c9221](https://github.com/hyperspell/python-sdk/commit/23c9221674f7d4d4d563b3e8d80d55c0b884114a))
+* **internal/types:** support eagerly validating pydantic iterators ([e47301a](https://github.com/hyperspell/python-sdk/commit/e47301a63173dd90013f6a2f127baf4cfd9e1553))
+* support setting headers via env ([eba0398](https://github.com/hyperspell/python-sdk/commit/eba039883cd57a0b9df0c4779d1370af59cd36ce))
+
+
+### Bug Fixes
+
+* **client:** add missing f-string prefix in file type error message ([6dbd3ec](https://github.com/hyperspell/python-sdk/commit/6dbd3ecf89c76051dde508e759d15db97acc4141))
+* use correct field name format for multipart file arrays ([0f569cc](https://github.com/hyperspell/python-sdk/commit/0f569cccc70ea96a2da668846b62bf65af449561))
+
+
+### Performance Improvements
+
+* **client:** optimize file structure copying in multipart requests ([c28640c](https://github.com/hyperspell/python-sdk/commit/c28640c64d25fd5dde07836e60e970069b0b65fe))
+
+
+### Chores
+
+* **internal:** more robust bootstrap script ([d4a39dc](https://github.com/hyperspell/python-sdk/commit/d4a39dc3c6f710bf9b9c481197043eeb4a9f8ac9))
+* **internal:** reformat pyproject.toml ([19a83c3](https://github.com/hyperspell/python-sdk/commit/19a83c32f936735d997727bda0bb0ba523f8086c))
+* **tests:** bump steady to v0.22.1 ([7d67d91](https://github.com/hyperspell/python-sdk/commit/7d67d91802b13e3110af4e1ab313cc36ed66ca74))
+
## 0.37.0 (2026-04-16)
Full Changelog: [v0.36.0...v0.37.0](https://github.com/hyperspell/python-sdk/compare/v0.36.0...v0.37.0)
diff --git a/README.md b/README.md
index 4b0ec922..da027aea 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/).
Use the Hyperspell MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
-[](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0)
-[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D)
+[](https://cursor.com/en-US/install-mcp?name=%40hyperspell%2Fhyperspell-mcp&config=eyJuYW1lIjoiQGh5cGVyc3BlbGwvaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40hyperspell%2Fhyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D)
> Note: You may need to set environment variables in your MCP client.
diff --git a/api.md b/api.md
index b7b2c535..a17e0cfa 100644
--- a/api.md
+++ b/api.md
@@ -1,7 +1,52 @@
# Shared Types
```python
-from hyperspell.types import Metadata, Notification, QueryResult, Resource
+from hyperspell.types import (
+ Blob,
+ Callout,
+ Chunk,
+ Code,
+ Comment,
+ Company,
+ Conversation,
+ Deal,
+ Divider,
+ Document,
+ Equation,
+ Event,
+ File,
+ Footnote,
+ Heading,
+ Image,
+ LineBreak,
+ Link,
+ List,
+ ListItem,
+ Message,
+ Metadata,
+ Paragraph,
+ Person,
+ Provenance,
+ ProvenanceEntity,
+ ProvenanceSource,
+ ProvenanceStep,
+ QueryResult,
+ Quote,
+ ScoredDocumentResponse,
+ Table,
+ TableCell,
+ TableRow,
+ Task,
+ Text,
+ ToDo,
+ ToolCall,
+ ToolResult,
+ Trace,
+ TraceMessage,
+ Transcript,
+ Utterance,
+ Website,
+)
```
# Connections
@@ -86,10 +131,11 @@ Types:
```python
from hyperspell.types import (
- Memory,
MemoryStatus,
+ MemoryListResponse,
MemoryDeleteResponse,
MemoryAddBulkResponse,
+ MemoryGetResponse,
MemoryStatusResponse,
)
```
@@ -97,11 +143,11 @@ from hyperspell.types import (
Methods:
- client.memories.update(resource_id, \*, source, \*\*params) -> MemoryStatus
-- client.memories.list(\*\*params) -> SyncCursorPage[Resource]
+- client.memories.list(\*\*params) -> SyncCursorPage[MemoryListResponse]
- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
- client.memories.add_bulk(\*\*params) -> MemoryAddBulkResponse
-- client.memories.get(resource_id, \*, source) -> Memory
+- client.memories.get(resource_id, \*, source) -> MemoryGetResponse
- client.memories.search(\*\*params) -> QueryResult
- client.memories.status() -> MemoryStatusResponse
- client.memories.upload(\*\*params) -> MemoryStatus
@@ -111,12 +157,17 @@ Methods:
Types:
```python
-from hyperspell.types import EvaluateScoreHighlightResponse, EvaluateScoreQueryResponse
+from hyperspell.types import (
+ EvaluateListQueriesResponse,
+ EvaluateScoreHighlightResponse,
+ EvaluateScoreQueryResponse,
+)
```
Methods:
- client.evaluate.get_query(query_id) -> QueryResult
+- client.evaluate.list_queries(\*\*params) -> SyncCursorPage[EvaluateListQueriesResponse]
- client.evaluate.score_highlight(highlight_id, \*\*params) -> EvaluateScoreHighlightResponse
- client.evaluate.score_query(query_id, \*\*params) -> EvaluateScoreQueryResponse
diff --git a/pyproject.toml b/pyproject.toml
index 7a6fa077..8ac832e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hyperspell"
-version = "0.37.0"
+version = "0.38.0"
description = "The official Python library for the hyperspell API"
dynamic = ["readme"]
license = "MIT"
@@ -154,7 +154,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/hyperspell/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/hyperspell/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 4638ec69..5a23841b 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
diff --git a/scripts/mock b/scripts/mock
index 5cd7c157..feebe5ed 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.20.2 -- steady --version
+ npm exec --package=@stdy/cli@0.22.1 -- steady --version
- npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index b754adab..a47c5b42 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py
index 78cfb089..20dbdf4a 100644
--- a/src/hyperspell/_client.py
+++ b/src/hyperspell/_client.py
@@ -19,7 +19,11 @@
RequestOptions,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ is_mapping_t,
+ get_async_library,
+)
from ._compat import cached_property
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
@@ -102,6 +106,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.hyperspell.com"
+ custom_headers_env = os.environ.get("HYPERSPELL_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
@@ -341,6 +354,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.hyperspell.com"
+ custom_headers_env = os.environ.get("HYPERSPELL_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py
index 155adfec..e5146d91 100644
--- a/src/hyperspell/_files.py
+++ b/src/hyperspell/_files.py
@@ -3,8 +3,8 @@
import io
import os
import pathlib
-from typing import overload
-from typing_extensions import TypeGuard
+from typing import Sequence, cast, overload
+from typing_extensions import TypeVar, TypeGuard
import anyio
@@ -17,7 +17,9 @@
HttpxFileContent,
HttpxRequestFiles,
)
-from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
+from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
+
+_T = TypeVar("_T")
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -97,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
- raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
+ raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
return files
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
return await anyio.Path(file).read_bytes()
return file
+
+
+def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
+ """Copy only the containers along the given paths.
+
+ Used to guard against mutation by extract_files without copying the entire structure.
+ Only dicts and lists that lie on a path are copied; everything else
+ is returned by reference.
+
+ For example, given paths=[["foo", "files", "file"]] and the structure:
+ {
+ "foo": {
+ "bar": {"baz": {}},
+ "files": {"file": }
+ }
+ }
+ The root dict, "foo", and "files" are copied (they lie on the path).
+ "bar" and "baz" are returned by reference (off the path).
+ """
+ return _deepcopy_with_paths(item, paths, 0)
+
+
+def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
+ if not paths:
+ return item
+ if is_mapping(item):
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
+ for path in paths:
+ if index < len(path):
+ key_to_paths.setdefault(path[index], []).append(path)
+
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
+ if not key_to_paths:
+ return item
+
+ result = dict(item)
+ for key, subpaths in key_to_paths.items():
+ if key in result:
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
+ return cast(_T, result)
+ if is_list(item):
+ array_paths = [path for path in paths if index < len(path) and path[index] == ""]
+
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
+ if not array_paths:
+ return cast(_T, item)
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
+ return item
diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py
index 29070e05..8c5ab260 100644
--- a/src/hyperspell/_models.py
+++ b/src/hyperspell/_models.py
@@ -25,7 +25,9 @@
ClassVar,
Protocol,
Required,
+ Annotated,
ParamSpec,
+ TypeAlias,
TypedDict,
TypeGuard,
final,
@@ -79,7 +81,15 @@
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
+ from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
+ from pydantic_core import CoreSchema, core_schema
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
+else:
+ try:
+ from pydantic_core import CoreSchema, core_schema
+ except ImportError:
+ CoreSchema = None
+ core_schema = None
__all__ = ["BaseModel", "GenericModel"]
@@ -396,6 +406,76 @@ def model_dump_json(
)
+class _EagerIterable(list[_T], Generic[_T]):
+ """
+ Accepts any Iterable[T] input (including generators), consumes it
+ eagerly, and validates all items upfront.
+
+ Validation preserves the original container type where possible
+ (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
+ always emits a list — round-tripping through model_dump() will not
+ restore the original container type.
+ """
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls,
+ source_type: Any,
+ handler: GetCoreSchemaHandler,
+ ) -> CoreSchema:
+ (item_type,) = get_args(source_type) or (Any,)
+ item_schema: CoreSchema = handler.generate_schema(item_type)
+ list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)
+
+ return core_schema.no_info_wrap_validator_function(
+ cls._validate,
+ list_of_items_schema,
+ serialization=core_schema.plain_serializer_function_ser_schema(
+ cls._serialize,
+ info_arg=False,
+ ),
+ )
+
+ @staticmethod
+ def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
+ original_type: type[Any] = type(v)
+
+ # Normalize to list so list_schema can validate each item
+ if isinstance(v, list):
+ items: list[_T] = v
+ else:
+ try:
+ items = list(v)
+ except TypeError as e:
+ raise TypeError("Value is not iterable") from e
+
+ # Validate items against the inner schema
+ validated: list[_T] = handler(items)
+
+ # Reconstruct original container type
+ if original_type is list:
+ return validated
+ # str(list) produces the list's repr, not a string built from items,
+ # so skip reconstruction for str and its subclasses.
+ if issubclass(original_type, str):
+ return validated
+ try:
+ return original_type(validated)
+ except (TypeError, ValueError):
+ # If the type cannot be reconstructed, just return the validated list
+ return validated
+
+ @staticmethod
+ def _serialize(v: Iterable[_T]) -> list[_T]:
+ """Always serialize as a list so Pydantic's JSON encoder is happy."""
+ if isinstance(v, list):
+ return v
+ return list(v)
+
+
+EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]
+
+
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
diff --git a/src/hyperspell/_qs.py b/src/hyperspell/_qs.py
index de8c99bc..4127c19c 100644
--- a/src/hyperspell/_qs.py
+++ b/src/hyperspell/_qs.py
@@ -2,17 +2,13 @@
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
-from typing_extensions import Literal, get_args
+from typing_extensions import get_args
-from ._types import NotGiven, not_given
+from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten
_T = TypeVar("_T")
-
-ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
-NestedFormat = Literal["dots", "brackets"]
-
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
diff --git a/src/hyperspell/_types.py b/src/hyperspell/_types.py
index c331e84c..a0a7f696 100644
--- a/src/hyperspell/_types.py
+++ b/src/hyperspell/_types.py
@@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
+ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
+NestedFormat = Literal["dots", "brackets"]
+
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
diff --git a/src/hyperspell/_utils/__init__.py b/src/hyperspell/_utils/__init__.py
index 10cb66d2..1c090e51 100644
--- a/src/hyperspell/_utils/__init__.py
+++ b/src/hyperspell/_utils/__init__.py
@@ -24,7 +24,6 @@
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
- deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
diff --git a/src/hyperspell/_utils/_utils.py b/src/hyperspell/_utils/_utils.py
index 63b8cd60..199cd231 100644
--- a/src/hyperspell/_utils/_utils.py
+++ b/src/hyperspell/_utils/_utils.py
@@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
-from typing_extensions import TypeGuard
+from typing_extensions import TypeGuard, get_args
import sniffio
-from .._types import Omit, NotGiven, FileTypes, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '', 'data'].
+ ``array_format`` controls how ```` segments contribute to the emitted
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files
+def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
+ if array_format == "brackets":
+ return "[]"
+ if array_format == "indices":
+ return f"[{array_index}]"
+ if array_format == "repeat" or array_format == "comma":
+ # Both repeat the bare field name for each file part; there is no
+ # meaningful way to comma-join binary parts.
+ return ""
+ raise NotImplementedError(
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
+ )
+
+
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
+ array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
- for entry in obj:
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ for array_index, entry in enumerate(obj):
+ suffix = _array_suffix(array_format, array_index)
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
+ assert_is_file_content(entry, key=emitted_key)
+ files.append((emitted_key, cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
@@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
+ array_format=array_format,
)
elif is_list(obj):
if key != "":
@@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
+ flattened_key=(
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
+ ),
+ array_format=array_format,
)
- for item in obj
+ for array_index, item in enumerate(obj)
]
)
@@ -177,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
-def deepcopy_minimal(item: _T) -> _T:
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
-
- - mappings, e.g. `dict`
- - list
-
- This is done for performance reasons.
- """
- if is_mapping(item):
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
- if is_list(item):
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
- return item
-
-
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py
index ea4e056d..db685eed 100644
--- a/src/hyperspell/_version.py
+++ b/src/hyperspell/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "hyperspell"
-__version__ = "0.37.0" # x-release-please-version
+__version__ = "0.38.0" # x-release-please-version
diff --git a/src/hyperspell/resources/actions.py b/src/hyperspell/resources/actions.py
index 2537a806..68fe5927 100644
--- a/src/hyperspell/resources/actions.py
+++ b/src/hyperspell/resources/actions.py
@@ -65,6 +65,15 @@ def add_reaction(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
timestamp: str,
connection: Optional[str] | Omit = omit,
@@ -133,6 +142,15 @@ def send_message(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
text: str,
channel: Optional[str] | Omit = omit,
@@ -226,6 +244,15 @@ async def add_reaction(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
timestamp: str,
connection: Optional[str] | Omit = omit,
@@ -294,6 +321,15 @@ async def send_message(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
text: str,
channel: Optional[str] | Omit = omit,
diff --git a/src/hyperspell/resources/evaluate.py b/src/hyperspell/resources/evaluate.py
index 02b1c4d1..5dee99db 100644
--- a/src/hyperspell/resources/evaluate.py
+++ b/src/hyperspell/resources/evaluate.py
@@ -6,7 +6,7 @@
import httpx
-from ..types import evaluate_score_query_params, evaluate_score_highlight_params
+from ..types import evaluate_score_query_params, evaluate_list_queries_params, evaluate_score_highlight_params
from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -17,9 +17,11 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncCursorPage, AsyncCursorPage
+from .._base_client import AsyncPaginator, make_request_options
from ..types.shared.query_result import QueryResult
from ..types.evaluate_score_query_response import EvaluateScoreQueryResponse
+from ..types.evaluate_list_queries_response import EvaluateListQueriesResponse
from ..types.evaluate_score_highlight_response import EvaluateScoreHighlightResponse
__all__ = ["EvaluateResource", "AsyncEvaluateResource"]
@@ -78,6 +80,56 @@ def get_query(
cast_to=QueryResult,
)
+ def list_queries(
+ self,
+ *,
+ cursor: Optional[str] | Omit = omit,
+ size: int | Omit = omit,
+ user_id: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncCursorPage[EvaluateListQueriesResponse]:
+ """
+ Paginate through all prior queries for the app, newest first.
+
+ User tokens only see their own queries; admin tokens see every query in the app
+ and can narrow to a single user with the `user_id` filter.
+
+ Args:
+ user_id: Filter queries by the user that issued them.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/evaluate/queries",
+ page=SyncCursorPage[EvaluateListQueriesResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "cursor": cursor,
+ "size": size,
+ "user_id": user_id,
+ },
+ evaluate_list_queries_params.EvaluateListQueriesParams,
+ ),
+ ),
+ model=EvaluateListQueriesResponse,
+ )
+
def score_highlight(
self,
highlight_id: str,
@@ -215,6 +267,56 @@ async def get_query(
cast_to=QueryResult,
)
+ def list_queries(
+ self,
+ *,
+ cursor: Optional[str] | Omit = omit,
+ size: int | Omit = omit,
+ user_id: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[EvaluateListQueriesResponse, AsyncCursorPage[EvaluateListQueriesResponse]]:
+ """
+ Paginate through all prior queries for the app, newest first.
+
+ User tokens only see their own queries; admin tokens see every query in the app
+ and can narrow to a single user with the `user_id` filter.
+
+ Args:
+ user_id: Filter queries by the user that issued them.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/evaluate/queries",
+ page=AsyncCursorPage[EvaluateListQueriesResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "cursor": cursor,
+ "size": size,
+ "user_id": user_id,
+ },
+ evaluate_list_queries_params.EvaluateListQueriesParams,
+ ),
+ ),
+ model=EvaluateListQueriesResponse,
+ )
+
async def score_highlight(
self,
highlight_id: str,
@@ -306,6 +408,9 @@ def __init__(self, evaluate: EvaluateResource) -> None:
self.get_query = to_raw_response_wrapper(
evaluate.get_query,
)
+ self.list_queries = to_raw_response_wrapper(
+ evaluate.list_queries,
+ )
self.score_highlight = to_raw_response_wrapper(
evaluate.score_highlight,
)
@@ -321,6 +426,9 @@ def __init__(self, evaluate: AsyncEvaluateResource) -> None:
self.get_query = async_to_raw_response_wrapper(
evaluate.get_query,
)
+ self.list_queries = async_to_raw_response_wrapper(
+ evaluate.list_queries,
+ )
self.score_highlight = async_to_raw_response_wrapper(
evaluate.score_highlight,
)
@@ -336,6 +444,9 @@ def __init__(self, evaluate: EvaluateResource) -> None:
self.get_query = to_streamed_response_wrapper(
evaluate.get_query,
)
+ self.list_queries = to_streamed_response_wrapper(
+ evaluate.list_queries,
+ )
self.score_highlight = to_streamed_response_wrapper(
evaluate.score_highlight,
)
@@ -351,6 +462,9 @@ def __init__(self, evaluate: AsyncEvaluateResource) -> None:
self.get_query = async_to_streamed_response_wrapper(
evaluate.get_query,
)
+ self.list_queries = async_to_streamed_response_wrapper(
+ evaluate.list_queries,
+ )
self.score_highlight = async_to_streamed_response_wrapper(
evaluate.score_highlight,
)
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index d731f93a..21fbf295 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -16,8 +16,9 @@
memory_upload_params,
memory_add_bulk_params,
)
+from .._files import deepcopy_with_paths
from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
-from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
+from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -28,10 +29,10 @@
)
from ..pagination import SyncCursorPage, AsyncCursorPage
from .._base_client import AsyncPaginator, make_request_options
-from ..types.memory import Memory
from ..types.memory_status import MemoryStatus
-from ..types.shared.resource import Resource
+from ..types.memory_get_response import MemoryGetResponse
from ..types.shared.query_result import QueryResult
+from ..types.memory_list_response import MemoryListResponse
from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_status_response import MemoryStatusResponse
from ..types.memory_add_bulk_response import MemoryAddBulkResponse
@@ -78,8 +79,18 @@ def update(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
collection: Union[str, object, None] | Omit = omit,
+ date: Union[Union[str, datetime], object, None] | Omit = omit,
metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit,
text: Union[str, object, None] | Omit = omit,
title: Union[str, object, None] | Omit = omit,
@@ -102,6 +113,8 @@ def update(
collection: The collection to move the document to — deprecated, set the collection using
metadata instead.
+ date: Date of the document for ranking and filtering.
+
metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max
64 chars. Values must be string, number, boolean, or null. Will be merged with
existing metadata.
@@ -127,6 +140,7 @@ def update(
body=maybe_transform(
{
"collection": collection,
+ "date": date,
"metadata": metadata,
"text": text,
"title": title,
@@ -162,6 +176,15 @@ def list(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
| Omit = omit,
@@ -173,7 +196,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorPage[Resource]:
+ ) -> SyncCursorPage[MemoryListResponse]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -200,7 +223,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=SyncCursorPage[Resource],
+ page=SyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -218,7 +241,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Resource,
+ model=MemoryListResponse,
)
def delete(
@@ -240,6 +263,15 @@ def delete(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -255,7 +287,9 @@ def delete(
operation deletes:
1. All chunks associated with the resource (including embeddings)
- 2. The resource record itself
+ 2. The documents row AND any legacy resources rows sharing the identity —
+ leaving either one behind would resurrect the memory through the double-read
+ path (ENG-2477).
Args: source: The document provider (e.g., gmail, notion, vault) resource_id:
The unique identifier of the resource to delete api_token: Authentication token
@@ -414,6 +448,15 @@ def get(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -421,9 +464,10 @@ def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
- Retrieves a document by provider and resource_id.
+ Retrieves a document by provider and resource_id, as a document-shaped response
+ carrying the full hyperdoc tree (ENG-2479 Phase 4).
Args:
extra_headers: Send extra headers
@@ -443,7 +487,7 @@ def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
def search(
@@ -451,9 +495,10 @@ def search(
*,
query: str,
answer: bool | Omit = omit,
- effort: int | Omit = omit,
+ effort: Literal["minimal", "low", "medium", "high", "very_high"] | Omit = omit,
max_results: int | Omit = omit,
options: memory_search_params.Options | Omit = omit,
+ provenance: bool | Omit = omit,
sources: List[
Literal[
"reddit",
@@ -470,6 +515,15 @@ def search(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
| Omit = omit,
@@ -488,13 +542,24 @@ def search(
answer: If true, the query will be answered along with matching source documents.
- effort: Effort level. 0 = pass query through verbatim. 1 = LLM rewrites the query for
- better retrieval and extracts date filters.
+ effort: How much compute to spend on retrieval. Mirrors the dial popularized by
+ frontier-model APIs (OpenAI reasoning_effort, etc.). 'minimal' = verbatim
+ single-shot retrieval (fastest). 'low' = LLM rewrites the query for better
+ retrieval and extracts date filters. 'medium' = rewrite + agentic refinement
+ loop (the answer LLM may request additional retrieval rounds, up to 3). 'high' =
+ rewrite + extended refinement (up to 6 rounds). Higher = better recall, more
+ latency, more cost.
max_results: Maximum number of results to return.
options: Search options for the query.
+ provenance:
+ If true (effort='very_high' only), attach a provenance record to the response:
+ the source documents and entities the answer was grounded in, the agent's search
+ trajectory, and any sources that failed. Adds one indexed lookup; intended for
+ auditability / compliance use cases.
+
sources: Only query documents from these sources.
extra_headers: Send extra headers
@@ -514,6 +579,7 @@ def search(
"effort": effort,
"max_results": max_results,
"options": options,
+ "provenance": provenance,
"sources": sources,
},
memory_search_params.MemorySearchParams,
@@ -583,12 +649,13 @@ def upload(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"collection": collection,
"metadata": metadata,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
@@ -645,8 +712,18 @@ async def update(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
collection: Union[str, object, None] | Omit = omit,
+ date: Union[Union[str, datetime], object, None] | Omit = omit,
metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit,
text: Union[str, object, None] | Omit = omit,
title: Union[str, object, None] | Omit = omit,
@@ -669,6 +746,8 @@ async def update(
collection: The collection to move the document to — deprecated, set the collection using
metadata instead.
+ date: Date of the document for ranking and filtering.
+
metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max
64 chars. Values must be string, number, boolean, or null. Will be merged with
existing metadata.
@@ -694,6 +773,7 @@ async def update(
body=await async_maybe_transform(
{
"collection": collection,
+ "date": date,
"metadata": metadata,
"text": text,
"title": title,
@@ -729,6 +809,15 @@ def list(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
| Omit = omit,
@@ -740,7 +829,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Resource, AsyncCursorPage[Resource]]:
+ ) -> AsyncPaginator[MemoryListResponse, AsyncCursorPage[MemoryListResponse]]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -767,7 +856,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=AsyncCursorPage[Resource],
+ page=AsyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -785,7 +874,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Resource,
+ model=MemoryListResponse,
)
async def delete(
@@ -807,6 +896,15 @@ async def delete(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -822,7 +920,9 @@ async def delete(
operation deletes:
1. All chunks associated with the resource (including embeddings)
- 2. The resource record itself
+ 2. The documents row AND any legacy resources rows sharing the identity —
+ leaving either one behind would resurrect the memory through the double-read
+ path (ENG-2477).
Args: source: The document provider (e.g., gmail, notion, vault) resource_id:
The unique identifier of the resource to delete api_token: Authentication token
@@ -981,6 +1081,15 @@ async def get(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -988,9 +1097,10 @@ async def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
- Retrieves a document by provider and resource_id.
+ Retrieves a document by provider and resource_id, as a document-shaped response
+ carrying the full hyperdoc tree (ENG-2479 Phase 4).
Args:
extra_headers: Send extra headers
@@ -1010,7 +1120,7 @@ async def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
async def search(
@@ -1018,9 +1128,10 @@ async def search(
*,
query: str,
answer: bool | Omit = omit,
- effort: int | Omit = omit,
+ effort: Literal["minimal", "low", "medium", "high", "very_high"] | Omit = omit,
max_results: int | Omit = omit,
options: memory_search_params.Options | Omit = omit,
+ provenance: bool | Omit = omit,
sources: List[
Literal[
"reddit",
@@ -1037,6 +1148,15 @@ async def search(
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
| Omit = omit,
@@ -1055,13 +1175,24 @@ async def search(
answer: If true, the query will be answered along with matching source documents.
- effort: Effort level. 0 = pass query through verbatim. 1 = LLM rewrites the query for
- better retrieval and extracts date filters.
+ effort: How much compute to spend on retrieval. Mirrors the dial popularized by
+ frontier-model APIs (OpenAI reasoning_effort, etc.). 'minimal' = verbatim
+ single-shot retrieval (fastest). 'low' = LLM rewrites the query for better
+ retrieval and extracts date filters. 'medium' = rewrite + agentic refinement
+ loop (the answer LLM may request additional retrieval rounds, up to 3). 'high' =
+ rewrite + extended refinement (up to 6 rounds). Higher = better recall, more
+ latency, more cost.
max_results: Maximum number of results to return.
options: Search options for the query.
+ provenance:
+ If true (effort='very_high' only), attach a provenance record to the response:
+ the source documents and entities the answer was grounded in, the agent's search
+ trajectory, and any sources that failed. Adds one indexed lookup; intended for
+ auditability / compliance use cases.
+
sources: Only query documents from these sources.
extra_headers: Send extra headers
@@ -1081,6 +1212,7 @@ async def search(
"effort": effort,
"max_results": max_results,
"options": options,
+ "provenance": provenance,
"sources": sources,
},
memory_search_params.MemorySearchParams,
@@ -1150,12 +1282,13 @@ async def upload(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"collection": collection,
"metadata": metadata,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index ce27266d..d77f4c69 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -2,9 +2,55 @@
from __future__ import annotations
+from . import shared, memory_get_response, memory_list_response
+from .. import _compat
from .token import Token as Token
-from .memory import Memory as Memory
-from .shared import Metadata as Metadata, Resource as Resource, QueryResult as QueryResult, Notification as Notification
+from .shared import (
+ Blob as Blob,
+ Code as Code,
+ Deal as Deal,
+ File as File,
+ Link as Link,
+ List as List,
+ Task as Task,
+ Text as Text,
+ ToDo as ToDo,
+ Chunk as Chunk,
+ Event as Event,
+ Image as Image,
+ Quote as Quote,
+ Table as Table,
+ Trace as Trace,
+ Person as Person,
+ Callout as Callout,
+ Comment as Comment,
+ Company as Company,
+ Divider as Divider,
+ Heading as Heading,
+ Message as Message,
+ Website as Website,
+ Document as Document,
+ Equation as Equation,
+ Footnote as Footnote,
+ ListItem as ListItem,
+ Metadata as Metadata,
+ TableRow as TableRow,
+ ToolCall as ToolCall,
+ LineBreak as LineBreak,
+ Paragraph as Paragraph,
+ TableCell as TableCell,
+ Utterance as Utterance,
+ Provenance as Provenance,
+ ToolResult as ToolResult,
+ Transcript as Transcript,
+ QueryResult as QueryResult,
+ Conversation as Conversation,
+ TraceMessage as TraceMessage,
+ ProvenanceStep as ProvenanceStep,
+ ProvenanceEntity as ProvenanceEntity,
+ ProvenanceSource as ProvenanceSource,
+ ScoredDocumentResponse as ScoredDocumentResponse,
+)
from .memory_status import MemoryStatus as MemoryStatus
from .auth_me_response import AuthMeResponse as AuthMeResponse
from .memory_add_params import MemoryAddParams as MemoryAddParams
@@ -12,8 +58,10 @@
from .folder_list_params import FolderListParams as FolderListParams
from .memory_list_params import MemoryListParams as MemoryListParams
from .session_add_params import SessionAddParams as SessionAddParams
+from .memory_get_response import MemoryGetResponse as MemoryGetResponse
from .vault_list_response import VaultListResponse as VaultListResponse
from .folder_list_response import FolderListResponse as FolderListResponse
+from .memory_list_response import MemoryListResponse as MemoryListResponse
from .memory_search_params import MemorySearchParams as MemorySearchParams
from .memory_update_params import MemoryUpdateParams as MemoryUpdateParams
from .memory_upload_params import MemoryUploadParams as MemoryUploadParams
@@ -33,10 +81,77 @@
from .evaluate_score_query_params import EvaluateScoreQueryParams as EvaluateScoreQueryParams
from .action_add_reaction_response import ActionAddReactionResponse as ActionAddReactionResponse
from .action_send_message_response import ActionSendMessageResponse as ActionSendMessageResponse
+from .evaluate_list_queries_params import EvaluateListQueriesParams as EvaluateListQueriesParams
from .folder_set_policies_response import FolderSetPoliciesResponse as FolderSetPoliciesResponse
from .integration_connect_response import IntegrationConnectResponse as IntegrationConnectResponse
from .evaluate_score_query_response import EvaluateScoreQueryResponse as EvaluateScoreQueryResponse
from .folder_delete_policy_response import FolderDeletePolicyResponse as FolderDeletePolicyResponse
from .folder_list_policies_response import FolderListPoliciesResponse as FolderListPoliciesResponse
+from .evaluate_list_queries_response import EvaluateListQueriesResponse as EvaluateListQueriesResponse
from .evaluate_score_highlight_params import EvaluateScoreHighlightParams as EvaluateScoreHighlightParams
from .evaluate_score_highlight_response import EvaluateScoreHighlightResponse as EvaluateScoreHighlightResponse
+
+# Rebuild cyclical models only after all modules are imported.
+# This ensures that, when building the deferred (due to cyclical references) model schema,
+# Pydantic can resolve the necessary references.
+# See: https://github.com/pydantic/pydantic/issues/11250 for more context.
+if _compat.PYDANTIC_V1:
+ memory_list_response.MemoryListResponse.update_forward_refs() # type: ignore
+ memory_get_response.MemoryGetResponse.update_forward_refs() # type: ignore
+ shared.callout.Callout.update_forward_refs() # type: ignore
+ shared.chunk.Chunk.update_forward_refs() # type: ignore
+ shared.company.Company.update_forward_refs() # type: ignore
+ shared.conversation.Conversation.update_forward_refs() # type: ignore
+ shared.deal.Deal.update_forward_refs() # type: ignore
+ shared.document.Document.update_forward_refs() # type: ignore
+ shared.equation.Equation.update_forward_refs() # type: ignore
+ shared.event.Event.update_forward_refs() # type: ignore
+ shared.file.File.update_forward_refs() # type: ignore
+ shared.footnote.Footnote.update_forward_refs() # type: ignore
+ shared.heading.Heading.update_forward_refs() # type: ignore
+ shared.list.List.update_forward_refs() # type: ignore
+ shared.list_item.ListItem.update_forward_refs() # type: ignore
+ shared.message.Message.update_forward_refs() # type: ignore
+ shared.paragraph.Paragraph.update_forward_refs() # type: ignore
+ shared.person.Person.update_forward_refs() # type: ignore
+ shared.query_result.QueryResult.update_forward_refs() # type: ignore
+ shared.quote.Quote.update_forward_refs() # type: ignore
+ shared.scored_document_response.ScoredDocumentResponse.update_forward_refs() # type: ignore
+ shared.table.Table.update_forward_refs() # type: ignore
+ shared.table_cell.TableCell.update_forward_refs() # type: ignore
+ shared.table_row.TableRow.update_forward_refs() # type: ignore
+ shared.task.Task.update_forward_refs() # type: ignore
+ shared.to_do.ToDo.update_forward_refs() # type: ignore
+ shared.transcript.Transcript.update_forward_refs() # type: ignore
+ shared.utterance.Utterance.update_forward_refs() # type: ignore
+ shared.website.Website.update_forward_refs() # type: ignore
+else:
+ memory_list_response.MemoryListResponse.model_rebuild(_parent_namespace_depth=0)
+ memory_get_response.MemoryGetResponse.model_rebuild(_parent_namespace_depth=0)
+ shared.callout.Callout.model_rebuild(_parent_namespace_depth=0)
+ shared.chunk.Chunk.model_rebuild(_parent_namespace_depth=0)
+ shared.company.Company.model_rebuild(_parent_namespace_depth=0)
+ shared.conversation.Conversation.model_rebuild(_parent_namespace_depth=0)
+ shared.deal.Deal.model_rebuild(_parent_namespace_depth=0)
+ shared.document.Document.model_rebuild(_parent_namespace_depth=0)
+ shared.equation.Equation.model_rebuild(_parent_namespace_depth=0)
+ shared.event.Event.model_rebuild(_parent_namespace_depth=0)
+ shared.file.File.model_rebuild(_parent_namespace_depth=0)
+ shared.footnote.Footnote.model_rebuild(_parent_namespace_depth=0)
+ shared.heading.Heading.model_rebuild(_parent_namespace_depth=0)
+ shared.list.List.model_rebuild(_parent_namespace_depth=0)
+ shared.list_item.ListItem.model_rebuild(_parent_namespace_depth=0)
+ shared.message.Message.model_rebuild(_parent_namespace_depth=0)
+ shared.paragraph.Paragraph.model_rebuild(_parent_namespace_depth=0)
+ shared.person.Person.model_rebuild(_parent_namespace_depth=0)
+ shared.query_result.QueryResult.model_rebuild(_parent_namespace_depth=0)
+ shared.quote.Quote.model_rebuild(_parent_namespace_depth=0)
+ shared.scored_document_response.ScoredDocumentResponse.model_rebuild(_parent_namespace_depth=0)
+ shared.table.Table.model_rebuild(_parent_namespace_depth=0)
+ shared.table_cell.TableCell.model_rebuild(_parent_namespace_depth=0)
+ shared.table_row.TableRow.model_rebuild(_parent_namespace_depth=0)
+ shared.task.Task.model_rebuild(_parent_namespace_depth=0)
+ shared.to_do.ToDo.model_rebuild(_parent_namespace_depth=0)
+ shared.transcript.Transcript.model_rebuild(_parent_namespace_depth=0)
+ shared.utterance.Utterance.model_rebuild(_parent_namespace_depth=0)
+ shared.website.Website.model_rebuild(_parent_namespace_depth=0)
diff --git a/src/hyperspell/types/action_add_reaction_params.py b/src/hyperspell/types/action_add_reaction_params.py
index a72da448..87e7b13d 100644
--- a/src/hyperspell/types/action_add_reaction_params.py
+++ b/src/hyperspell/types/action_add_reaction_params.py
@@ -31,6 +31,15 @@ class ActionAddReactionParams(TypedDict, total=False):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""Integration provider (e.g., slack)"""
diff --git a/src/hyperspell/types/action_send_message_params.py b/src/hyperspell/types/action_send_message_params.py
index 2df01983..bc76880d 100644
--- a/src/hyperspell/types/action_send_message_params.py
+++ b/src/hyperspell/types/action_send_message_params.py
@@ -25,6 +25,15 @@ class ActionSendMessageParams(TypedDict, total=False):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""Integration provider (e.g., slack)"""
diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py
index 93ea0975..918e0e73 100644
--- a/src/hyperspell/types/auth_me_response.py
+++ b/src/hyperspell/types/auth_me_response.py
@@ -48,6 +48,15 @@ class AuthMeResponse(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""All integrations available for the app"""
@@ -68,6 +77,15 @@ class AuthMeResponse(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""All integrations installed for the user"""
diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py
index 8092529b..6c5feef3 100644
--- a/src/hyperspell/types/connection_list_response.py
+++ b/src/hyperspell/types/connection_list_response.py
@@ -33,9 +33,24 @@ class Connection(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
"""The connection's provider"""
+ selected_count: Optional[int] = None
+ """
+ Count of items in user_options.channels (Teams: workspaces selected; 0 means
+ nothing is being indexed for integrations that require selection).
+ """
+
class ConnectionListResponse(BaseModel):
connections: List[Connection]
diff --git a/src/hyperspell/types/evaluate_list_queries_params.py b/src/hyperspell/types/evaluate_list_queries_params.py
new file mode 100644
index 00000000..2c9b765d
--- /dev/null
+++ b/src/hyperspell/types/evaluate_list_queries_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import TypedDict
+
+__all__ = ["EvaluateListQueriesParams"]
+
+
+class EvaluateListQueriesParams(TypedDict, total=False):
+ cursor: Optional[str]
+
+ size: int
+
+ user_id: Optional[str]
+ """Filter queries by the user that issued them."""
diff --git a/src/hyperspell/types/evaluate_list_queries_response.py b/src/hyperspell/types/evaluate_list_queries_response.py
new file mode 100644
index 00000000..1f1692d1
--- /dev/null
+++ b/src/hyperspell/types/evaluate_list_queries_response.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["EvaluateListQueriesResponse"]
+
+
+class EvaluateListQueriesResponse(BaseModel):
+ query: str
+ """The query string that was issued."""
+
+ query_id: str
+ """The ID of the query."""
+
+ time: datetime
+ """When the query was issued."""
+
+ user_id: Optional[str] = None
+ """The ID of the user that issued the query, if any."""
diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py
index 47292e38..02f10164 100644
--- a/src/hyperspell/types/integration_list_response.py
+++ b/src/hyperspell/types/integration_list_response.py
@@ -39,12 +39,24 @@ class Integration(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
"""The integration's provider"""
actions_only: Optional[bool] = None
"""Whether this integration only supports write actions (no sync)"""
+ requires_channel_selection: Optional[bool] = None
+ """Whether the user must select channels before indexing starts"""
+
class IntegrationListResponse(BaseModel):
integrations: List[Integration]
diff --git a/src/hyperspell/types/integrations/web_crawler_index_response.py b/src/hyperspell/types/integrations/web_crawler_index_response.py
index 16dc13e3..bb44ee7b 100644
--- a/src/hyperspell/types/integrations/web_crawler_index_response.py
+++ b/src/hyperspell/types/integrations/web_crawler_index_response.py
@@ -25,6 +25,15 @@ class WebCrawlerIndexResponse(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
status: Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]
diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py
deleted file mode 100644
index 4ac829ef..00000000
--- a/src/hyperspell/types/memory.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import TYPE_CHECKING, Dict, List, Optional
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-from .shared.metadata import Metadata
-
-__all__ = ["Memory"]
-
-
-class Memory(BaseModel):
- """Response model for the GET /memories/get endpoint."""
-
- resource_id: str
-
- source: Literal[
- "reddit",
- "notion",
- "slack",
- "google_calendar",
- "google_mail",
- "box",
- "dropbox",
- "github",
- "google_drive",
- "vault",
- "web_crawler",
- "trace",
- "microsoft_teams",
- "gmail_actions",
- ]
-
- type: str
- """The type of document (e.g. Document, Website, Email)"""
-
- data: Optional[List[object]] = None
- """The structured content of the document"""
-
- memories: Optional[List[str]] = None
- """Summaries of all memories extracted from this document"""
-
- metadata: Optional[Metadata] = None
-
- title: Optional[str] = None
-
- if TYPE_CHECKING:
- # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
- # value to this field, so for compatibility we avoid doing it at runtime.
- __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
-
- # Stub to indicate that arbitrary properties are accepted.
- # To access properties that are not valid identifiers you can use `getattr`, e.g.
- # `getattr(obj, '$type')`
- def __getattr__(self, attr: str) -> object: ...
- else:
- __pydantic_extra__: Dict[str, object]
diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py
index 5f0432d1..1b1b262b 100644
--- a/src/hyperspell/types/memory_delete_response.py
+++ b/src/hyperspell/types/memory_delete_response.py
@@ -29,6 +29,15 @@ class MemoryDeleteResponse(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
success: bool
diff --git a/src/hyperspell/types/memory_get_response.py b/src/hyperspell/types/memory_get_response.py
new file mode 100644
index 00000000..7f701601
--- /dev/null
+++ b/src/hyperspell/types/memory_get_response.py
@@ -0,0 +1,107 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .._models import BaseModel
+from .shared.trace import Trace
+
+__all__ = ["MemoryGetResponse", "Document"]
+
+Document: TypeAlias = Annotated[
+ Union[
+ "document.Document",
+ "Website",
+ "Task",
+ "Person",
+ "Message",
+ "Event",
+ "File",
+ "Conversation",
+ Trace,
+ "Transcript",
+ "Company",
+ "Deal",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class MemoryGetResponse(BaseModel):
+ """A document-shaped API response carrying the hyperdoc tree (ENG-2479/D12)."""
+
+ document: Document
+ """The full hyperdoc tree.
+
+ Switch on `type` for the document frame and recurse `children` for the body —
+ see the `` renderer.
+ """
+
+ resource_id: str
+
+ source: Literal[
+ "reddit",
+ "notion",
+ "slack",
+ "google_calendar",
+ "google_mail",
+ "box",
+ "dropbox",
+ "github",
+ "google_drive",
+ "vault",
+ "web_crawler",
+ "trace",
+ "microsoft_teams",
+ "gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
+ ]
+
+ type: str
+ """Hyperdoc document type discriminator (document, message, file, event, ...)."""
+
+ collection: Optional[str] = None
+ """The document's collection, if any."""
+
+ document_date: Optional[datetime] = None
+ """The document's own date (e.g. email sent date, event date)."""
+
+ ingested_at: Optional[datetime] = None
+ """When Hyperspell first indexed the document."""
+
+ last_modified_at: Optional[datetime] = None
+ """When the source document was last modified."""
+
+ metadata: Optional[Dict[str, object]] = None
+ """Filterable custom metadata attached to the document."""
+
+ status: Optional[Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]] = None
+ """Indexing status of the document."""
+
+ title: Optional[str] = None
+ """Human-readable document title."""
+
+
+from .shared import document
+from .shared.deal import Deal
+from .shared.file import File
+from .shared.task import Task
+from .shared.event import Event
+from .shared.person import Person
+from .shared.company import Company
+from .shared.message import Message
+from .shared.website import Website
+from .shared.transcript import Transcript
+from .shared.conversation import Conversation
diff --git a/src/hyperspell/types/memory_list_params.py b/src/hyperspell/types/memory_list_params.py
index 319f2917..300c50a7 100644
--- a/src/hyperspell/types/memory_list_params.py
+++ b/src/hyperspell/types/memory_list_params.py
@@ -38,6 +38,15 @@ class MemoryListParams(TypedDict, total=False):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""Filter documents by source."""
diff --git a/src/hyperspell/types/memory_list_response.py b/src/hyperspell/types/memory_list_response.py
new file mode 100644
index 00000000..aa6664c1
--- /dev/null
+++ b/src/hyperspell/types/memory_list_response.py
@@ -0,0 +1,107 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .._models import BaseModel
+from .shared.trace import Trace
+
+__all__ = ["MemoryListResponse", "Document"]
+
+Document: TypeAlias = Annotated[
+ Union[
+ "document.Document",
+ "Website",
+ "Task",
+ "Person",
+ "Message",
+ "Event",
+ "File",
+ "Conversation",
+ Trace,
+ "Transcript",
+ "Company",
+ "Deal",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class MemoryListResponse(BaseModel):
+ """A document-shaped API response carrying the hyperdoc tree (ENG-2479/D12)."""
+
+ document: Document
+ """The full hyperdoc tree.
+
+ Switch on `type` for the document frame and recurse `children` for the body —
+ see the `` renderer.
+ """
+
+ resource_id: str
+
+ source: Literal[
+ "reddit",
+ "notion",
+ "slack",
+ "google_calendar",
+ "google_mail",
+ "box",
+ "dropbox",
+ "github",
+ "google_drive",
+ "vault",
+ "web_crawler",
+ "trace",
+ "microsoft_teams",
+ "gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
+ ]
+
+ type: str
+ """Hyperdoc document type discriminator (document, message, file, event, ...)."""
+
+ collection: Optional[str] = None
+ """The document's collection, if any."""
+
+ document_date: Optional[datetime] = None
+ """The document's own date (e.g. email sent date, event date)."""
+
+ ingested_at: Optional[datetime] = None
+ """When Hyperspell first indexed the document."""
+
+ last_modified_at: Optional[datetime] = None
+ """When the source document was last modified."""
+
+ metadata: Optional[Dict[str, object]] = None
+ """Filterable custom metadata attached to the document."""
+
+ status: Optional[Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]] = None
+ """Indexing status of the document."""
+
+ title: Optional[str] = None
+ """Human-readable document title."""
+
+
+from .shared import document
+from .shared.deal import Deal
+from .shared.file import File
+from .shared.task import Task
+from .shared.event import Event
+from .shared.person import Person
+from .shared.company import Company
+from .shared.message import Message
+from .shared.website import Website
+from .shared.transcript import Transcript
+from .shared.conversation import Conversation
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index 5719262c..ef256252 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -17,7 +17,6 @@
"OptionsGoogleDrive",
"OptionsGoogleMail",
"OptionsNotion",
- "OptionsReddit",
"OptionsSlack",
"OptionsVault",
"OptionsWebCrawler",
@@ -31,11 +30,15 @@ class MemorySearchParams(TypedDict, total=False):
answer: bool
"""If true, the query will be answered along with matching source documents."""
- effort: int
- """Effort level.
+ effort: Literal["minimal", "low", "medium", "high", "very_high"]
+ """How much compute to spend on retrieval.
- 0 = pass query through verbatim. 1 = LLM rewrites the query for better retrieval
- and extracts date filters.
+ Mirrors the dial popularized by frontier-model APIs (OpenAI reasoning_effort,
+ etc.). 'minimal' = verbatim single-shot retrieval (fastest). 'low' = LLM
+ rewrites the query for better retrieval and extracts date filters. 'medium' =
+ rewrite + agentic refinement loop (the answer LLM may request additional
+ retrieval rounds, up to 3). 'high' = rewrite + extended refinement (up to 6
+ rounds). Higher = better recall, more latency, more cost.
"""
max_results: int
@@ -44,6 +47,14 @@ class MemorySearchParams(TypedDict, total=False):
options: Options
"""Search options for the query."""
+ provenance: bool
+ """
+ If true (effort='very_high' only), attach a provenance record to the response:
+ the source documents and entities the answer was grounded in, the agent's search
+ trajectory, and any sources that failed. Adds one indexed lookup; intended for
+ auditability / compliance use cases.
+ """
+
sources: List[
Literal[
"reddit",
@@ -60,6 +71,15 @@ class MemorySearchParams(TypedDict, total=False):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
"""Only query documents from these sources."""
@@ -146,30 +166,6 @@ class OptionsNotion(TypedDict, total=False):
"""
-class OptionsReddit(TypedDict, total=False):
- """Search options for Reddit"""
-
- period: Literal["hour", "day", "week", "month", "year", "all"]
- """The time period to search. Defaults to 'month'."""
-
- sort: Literal["relevance", "new", "hot", "top", "comments"]
- """The sort order of the posts. Defaults to 'relevance'."""
-
- subreddit: Optional[str]
- """The subreddit to search.
-
- If not provided, the query will be searched for in all subreddits.
- """
-
- weight: float
- """Weight of results from this source.
-
- A weight greater than 1.0 means more results from this source will be returned,
- a weight less than 1.0 means fewer results will be returned. This will only
- affect results if multiple sources are queried at the same time.
- """
-
-
class OptionsSlack(TypedDict, total=False):
"""Search options for Slack"""
@@ -275,8 +271,14 @@ class Options(TypedDict, total=False):
notion: OptionsNotion
"""Search options for Notion"""
- reddit: OptionsReddit
- """Search options for Reddit"""
+ recency_half_life_days: Optional[float]
+ """
+ When set, multiplies each result's score by an exponential-decay factor based on
+ the document's most recent activity timestamp (source-reported last_modified,
+ falling back to document_date). A document one half-life old gets its score
+ halved. Resources with no recency timestamp are passed through unchanged. Leave
+ unset to disable.
+ """
resource_ids: Optional[SequenceNotStr[str]]
"""Only return results from these specific resource IDs.
diff --git a/src/hyperspell/types/memory_status.py b/src/hyperspell/types/memory_status.py
index f30b7c73..47474f7c 100644
--- a/src/hyperspell/types/memory_status.py
+++ b/src/hyperspell/types/memory_status.py
@@ -25,6 +25,15 @@ class MemoryStatus(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
status: Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]
diff --git a/src/hyperspell/types/memory_update_params.py b/src/hyperspell/types/memory_update_params.py
index d8859d62..0e543114 100644
--- a/src/hyperspell/types/memory_update_params.py
+++ b/src/hyperspell/types/memory_update_params.py
@@ -3,7 +3,10 @@
from __future__ import annotations
from typing import Dict, Union
-from typing_extensions import Literal, Required, TypedDict
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
__all__ = ["MemoryUpdateParams"]
@@ -25,6 +28,15 @@ class MemoryUpdateParams(TypedDict, total=False):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
]
@@ -34,6 +46,9 @@ class MemoryUpdateParams(TypedDict, total=False):
metadata instead.
"""
+ date: Annotated[Union[Union[str, datetime], object, None], PropertyInfo(format="iso8601")]
+ """Date of the document for ranking and filtering."""
+
metadata: Union[Dict[str, Union[str, float, bool, None]], object, None]
"""Custom metadata for filtering.
diff --git a/src/hyperspell/types/shared/__init__.py b/src/hyperspell/types/shared/__init__.py
index ffb125a3..e07083a8 100644
--- a/src/hyperspell/types/shared/__init__.py
+++ b/src/hyperspell/types/shared/__init__.py
@@ -1,6 +1,46 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from .blob import Blob as Blob
+from .code import Code as Code
+from .deal import Deal as Deal
+from .file import File as File
+from .link import Link as Link
+from .list import List as List
+from .task import Task as Task
+from .text import Text as Text
+from .chunk import Chunk as Chunk
+from .event import Event as Event
+from .image import Image as Image
+from .quote import Quote as Quote
+from .table import Table as Table
+from .to_do import ToDo as ToDo
+from .trace import Trace as Trace
+from .person import Person as Person
+from .callout import Callout as Callout
+from .comment import Comment as Comment
+from .company import Company as Company
+from .divider import Divider as Divider
+from .heading import Heading as Heading
+from .message import Message as Message
+from .website import Website as Website
+from .document import Document as Document
+from .equation import Equation as Equation
+from .footnote import Footnote as Footnote
from .metadata import Metadata as Metadata
-from .resource import Resource as Resource
-from .notification import Notification as Notification
+from .list_item import ListItem as ListItem
+from .paragraph import Paragraph as Paragraph
+from .table_row import TableRow as TableRow
+from .tool_call import ToolCall as ToolCall
+from .utterance import Utterance as Utterance
+from .line_break import LineBreak as LineBreak
+from .provenance import Provenance as Provenance
+from .table_cell import TableCell as TableCell
+from .transcript import Transcript as Transcript
+from .tool_result import ToolResult as ToolResult
+from .conversation import Conversation as Conversation
from .query_result import QueryResult as QueryResult
+from .trace_message import TraceMessage as TraceMessage
+from .provenance_step import ProvenanceStep as ProvenanceStep
+from .provenance_entity import ProvenanceEntity as ProvenanceEntity
+from .provenance_source import ProvenanceSource as ProvenanceSource
+from .scored_document_response import ScoredDocumentResponse as ScoredDocumentResponse
diff --git a/src/hyperspell/types/shared/blob.py b/src/hyperspell/types/shared/blob.py
new file mode 100644
index 00000000..b30b7dd3
--- /dev/null
+++ b/src/hyperspell/types/shared/blob.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Blob"]
+
+
+class Blob(BaseModel):
+ """Represents embedded binary data using data URI scheme.
+
+ Format: data:[][;base64],
+ Example: data:text/html;base64,PGh0bWw+...
+ """
+
+ data: str
+
+ mimetype: str
+
+ id: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["blob"]] = None
diff --git a/src/hyperspell/types/shared/callout.py b/src/hyperspell/types/shared/callout.py
new file mode 100644
index 00000000..ccaad590
--- /dev/null
+++ b/src/hyperspell/types/shared/callout.py
@@ -0,0 +1,129 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Callout", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Callout(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["callout"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/chunk.py b/src/hyperspell/types/shared/chunk.py
new file mode 100644
index 00000000..42d7cc8b
--- /dev/null
+++ b/src/hyperspell/types/shared/chunk.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Chunk", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Chunk(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["chunk"]] = None
+
+
+from .list import List as ListList
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/code.py b/src/hyperspell/types/shared/code.py
new file mode 100644
index 00000000..15aba371
--- /dev/null
+++ b/src/hyperspell/types/shared/code.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Code"]
+
+
+class Code(BaseModel):
+ text: str
+
+ id: Optional[str] = None
+
+ language: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["code"]] = None
diff --git a/src/hyperspell/types/shared/comment.py b/src/hyperspell/types/shared/comment.py
new file mode 100644
index 00000000..7c93c996
--- /dev/null
+++ b/src/hyperspell/types/shared/comment.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Comment"]
+
+
+class Comment(BaseModel):
+ text: str
+
+ id: Optional[str] = None
+
+ created_at: Optional[datetime] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["comment"]] = None
diff --git a/src/hyperspell/types/shared/company.py b/src/hyperspell/types/shared/company.py
new file mode 100644
index 00000000..20642049
--- /dev/null
+++ b/src/hyperspell/types/shared/company.py
@@ -0,0 +1,122 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Company", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Company(BaseModel):
+ """A CRM company/account record (ENG-2476/D10)."""
+
+ id: Optional[str] = None
+
+ address: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ contact_ids: Optional[TypingList[str]] = None
+
+ deal_ids: Optional[TypingList[str]] = None
+
+ description: Optional[str] = None
+
+ emails: Optional[TypingList[str]] = None
+
+ employees: Optional[int] = None
+
+ image_url: Optional[str] = None
+
+ industry: Optional[str] = None
+
+ is_active: Optional[bool] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ name: Optional[str] = None
+
+ phone_numbers: Optional[TypingList[str]] = None
+
+ tags: Optional[TypingList[str]] = None
+
+ text: Optional[str] = None
+
+ timezone: Optional[str] = None
+
+ type: Optional[Literal["company"]] = None
+
+ websites: Optional[TypingList[str]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/conversation.py b/src/hyperspell/types/shared/conversation.py
new file mode 100644
index 00000000..96552797
--- /dev/null
+++ b/src/hyperspell/types/shared/conversation.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Conversation"]
+
+
+class Conversation(BaseModel):
+ id: Optional[str] = None
+
+ channel: Optional[str] = None
+
+ children: Optional[List["Message"]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["conversation"]] = None
+
+
+from .message import Message
diff --git a/src/hyperspell/types/shared/deal.py b/src/hyperspell/types/shared/deal.py
new file mode 100644
index 00000000..60bad8e0
--- /dev/null
+++ b/src/hyperspell/types/shared/deal.py
@@ -0,0 +1,121 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Deal", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Deal(BaseModel):
+ """A CRM deal/opportunity record (ENG-2476/D10)."""
+
+ id: Optional[str] = None
+
+ amount: Optional[float] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ closed_at: Optional[datetime] = None
+
+ company_ids: Optional[TypingList[str]] = None
+
+ contact_ids: Optional[TypingList[str]] = None
+
+ currency: Optional[str] = None
+
+ deal_source: Optional[str] = None
+
+ lost_reason: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ name: Optional[str] = None
+
+ pipeline: Optional[str] = None
+
+ probability: Optional[float] = None
+
+ stage: Optional[str] = None
+
+ tags: Optional[TypingList[str]] = None
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["deal"]] = None
+
+ won_reason: Optional[str] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/divider.py b/src/hyperspell/types/shared/divider.py
new file mode 100644
index 00000000..17c80be2
--- /dev/null
+++ b/src/hyperspell/types/shared/divider.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Divider"]
+
+
+class Divider(BaseModel):
+ id: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["divider"]] = None
diff --git a/src/hyperspell/types/shared/document.py b/src/hyperspell/types/shared/document.py
new file mode 100644
index 00000000..1e9e3ee2
--- /dev/null
+++ b/src/hyperspell/types/shared/document.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Document", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Document(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["document"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/equation.py b/src/hyperspell/types/shared/equation.py
new file mode 100644
index 00000000..6343cf8f
--- /dev/null
+++ b/src/hyperspell/types/shared/equation.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Equation", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Equation(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["equation"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/event.py b/src/hyperspell/types/shared/event.py
new file mode 100644
index 00000000..303c264b
--- /dev/null
+++ b/src/hyperspell/types/shared/event.py
@@ -0,0 +1,106 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Event", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Event(BaseModel):
+ id: Optional[str] = None
+
+ attendees: Optional[TypingList["Person"]] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ end_at: Optional[datetime] = None
+
+ location: Optional[str] = None
+
+ meeting_url: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ start_at: Optional[datetime] = None
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["event"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .person import Person
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/file.py b/src/hyperspell/types/shared/file.py
new file mode 100644
index 00000000..52657f26
--- /dev/null
+++ b/src/hyperspell/types/shared/file.py
@@ -0,0 +1,100 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["File", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class File(BaseModel):
+ content_type: str
+
+ filename: str
+
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ path: Optional[TypingList[str]] = None
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["file"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/footnote.py b/src/hyperspell/types/shared/footnote.py
new file mode 100644
index 00000000..d604fb5d
--- /dev/null
+++ b/src/hyperspell/types/shared/footnote.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Footnote", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Footnote(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["footnote"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/heading.py b/src/hyperspell/types/shared/heading.py
new file mode 100644
index 00000000..752520ce
--- /dev/null
+++ b/src/hyperspell/types/shared/heading.py
@@ -0,0 +1,129 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Heading", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Heading(BaseModel):
+ level: int
+
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["heading"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/image.py b/src/hyperspell/types/shared/image.py
new file mode 100644
index 00000000..3ee29e68
--- /dev/null
+++ b/src/hyperspell/types/shared/image.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Image"]
+
+
+class Image(BaseModel):
+ src: str
+
+ text: str
+
+ id: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["image"]] = None
diff --git a/src/hyperspell/types/shared/line_break.py b/src/hyperspell/types/shared/line_break.py
new file mode 100644
index 00000000..ad4c4e28
--- /dev/null
+++ b/src/hyperspell/types/shared/line_break.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["LineBreak"]
+
+
+class LineBreak(BaseModel):
+ id: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["line_break"]] = None
diff --git a/src/hyperspell/types/shared/link.py b/src/hyperspell/types/shared/link.py
new file mode 100644
index 00000000..3a6676e8
--- /dev/null
+++ b/src/hyperspell/types/shared/link.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Link"]
+
+
+class Link(BaseModel):
+ text: str
+
+ url: str
+
+ id: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["link"]] = None
diff --git a/src/hyperspell/types/shared/list.py b/src/hyperspell/types/shared/list.py
new file mode 100644
index 00000000..ae53ac45
--- /dev/null
+++ b/src/hyperspell/types/shared/list.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import typing
+from typing_extensions import Literal, TypeAlias, TypeAliasType
+
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+
+__all__ = ["List", "Child"]
+
+if typing.TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType("Child", typing.Union["ListItem", "ToDo"])
+else:
+ Child: TypeAlias = typing.Union["ListItem", "ToDo"]
+
+
+class List(BaseModel):
+ id: typing.Optional[str] = None
+
+ children: typing.Optional[typing.List[Child]] = None
+
+ metadata: typing.Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ ordered: typing.Optional[bool] = None
+
+ text: typing.Optional[str] = None
+
+ type: typing.Optional[Literal["list"]] = None
+
+
+from .to_do import ToDo
+from .list_item import ListItem
diff --git a/src/hyperspell/types/shared/list_item.py b/src/hyperspell/types/shared/list_item.py
new file mode 100644
index 00000000..a24bef61
--- /dev/null
+++ b/src/hyperspell/types/shared/list_item.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["ListItem", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class ListItem(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["list_item"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/message.py b/src/hyperspell/types/shared/message.py
new file mode 100644
index 00000000..c63ae448
--- /dev/null
+++ b/src/hyperspell/types/shared/message.py
@@ -0,0 +1,126 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Message", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Message(BaseModel):
+ date: datetime
+
+ sender: "Person"
+
+ id: Optional[str] = None
+
+ channel: Optional[str] = None
+ """
+ The channel or platform where the message was posted, if this Message is not
+ explicitly part of a conversation
+ """
+
+ children: Optional[TypingList[Child]] = None
+
+ external_id: Optional[str] = None
+ """Provider message id (e.g. Slack ts, Gmail message id) — merge-dedup key"""
+
+ is_self: Optional[bool] = None
+
+ mentioned_users: Optional[TypingList["Person"]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ num_replies: Optional[int] = None
+
+ replies: Optional[TypingList["Message"]] = None
+ """The replies or comments to the message"""
+
+ text: Optional[str] = None
+
+ thread_id: Optional[str] = None
+
+ title: Optional[str] = None
+ """The subject or title of the message"""
+
+ type: Optional[Literal["message"]] = None
+
+ updated_at: Optional[datetime] = None
+
+ upvotes: Optional[int] = None
+ """The number of upvotes, likes, or reactions on the message"""
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .person import Person
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/metadata.py b/src/hyperspell/types/shared/metadata.py
index d433c051..42aea64c 100644
--- a/src/hyperspell/types/shared/metadata.py
+++ b/src/hyperspell/types/shared/metadata.py
@@ -1,38 +1,43 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import TYPE_CHECKING, Dict, List, Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
+from typing import List, Optional
from ..._models import BaseModel
-from .notification import Notification
-__all__ = ["Metadata"]
+__all__ = ["Metadata", "Source"]
-class Metadata(BaseModel):
- created_at: Optional[datetime] = None
+class Source(BaseModel):
+ """A reference to a memory/chunk that a block's content is grounded in (ENG-1390).
+
+ Chunks are the unit persisted to the DB — extracted memories become chunks when
+ indexed — so `chunk_id` is the stable pointer back to the source. `resource_id`
+ and `source` locate the originating document; `score` carries optional retrieval
+ relevance. Kept deliberately self-contained (plain `str` for `source` rather than
+ the `DocumentProviders` enum) so the hyperdoc format stays free of app-layer imports.
+ """
+
+ chunk_id: str
- events: Optional[List[Notification]] = None
+ resource_id: Optional[str] = None
- indexed_at: Optional[datetime] = None
+ score: Optional[float] = None
- last_modified: Optional[datetime] = None
+ source: Optional[str] = None
+
+
+class Metadata(BaseModel):
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
- status: Optional[Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]] = None
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
- url: Optional[str] = None
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
- if TYPE_CHECKING:
- # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
- # value to this field, so for compatibility we avoid doing it at runtime.
- __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+ edited_by: Optional[str] = None
- # Stub to indicate that arbitrary properties are accepted.
- # To access properties that are not valid identifiers you can use `getattr`, e.g.
- # `getattr(obj, '$type')`
- def __getattr__(self, attr: str) -> object: ...
- else:
- __pydantic_extra__: Dict[str, object]
+ sources: Optional[List[Source]] = None
diff --git a/src/hyperspell/types/shared/notification.py b/src/hyperspell/types/shared/notification.py
deleted file mode 100644
index a78f858d..00000000
--- a/src/hyperspell/types/shared/notification.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from ..._models import BaseModel
-
-__all__ = ["Notification"]
-
-
-class Notification(BaseModel):
- message: str
-
- type: Literal["error", "warning", "info", "success"]
-
- time: Optional[datetime] = None
diff --git a/src/hyperspell/types/shared/paragraph.py b/src/hyperspell/types/shared/paragraph.py
new file mode 100644
index 00000000..e2a4a3eb
--- /dev/null
+++ b/src/hyperspell/types/shared/paragraph.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Paragraph", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Paragraph(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["paragraph"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/person.py b/src/hyperspell/types/shared/person.py
new file mode 100644
index 00000000..fed40d03
--- /dev/null
+++ b/src/hyperspell/types/shared/person.py
@@ -0,0 +1,160 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from datetime import date
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Person", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Person(BaseModel):
+ id: Optional[str] = None
+
+ address: Optional[str] = None
+
+ alt_names: Optional[TypingList[str]] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ company: Optional[str] = None
+
+ company_ids: Optional[TypingList[str]] = None
+
+ date_of_birth: Optional[date] = None
+
+ deal_ids: Optional[TypingList[str]] = None
+
+ email: Optional[str] = None
+
+ emails: Optional[TypingList[str]] = None
+ """All known email addresses; `email` holds the primary one"""
+
+ image_url: Optional[str] = None
+
+ job_title: Optional[str] = None
+
+ link_urls: Optional[TypingList[str]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ name: Optional[str] = None
+
+ phone_numbers: Optional[TypingList[str]] = None
+
+ tags: Optional[TypingList[str]] = None
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["person"]] = None
+
+ username: Optional[str] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/provenance.py b/src/hyperspell/types/shared/provenance.py
new file mode 100644
index 00000000..fbd6c40e
--- /dev/null
+++ b/src/hyperspell/types/shared/provenance.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ..._models import BaseModel
+from .provenance_step import ProvenanceStep
+from .provenance_entity import ProvenanceEntity
+from .provenance_source import ProvenanceSource
+
+__all__ = ["Provenance"]
+
+
+class Provenance(BaseModel):
+ """Auditability record attached to an agentic answer.
+
+ Gated behind ``provenance=true`` on the request: the cheap parts (sources,
+ steps, failed_sources) are derived from in-memory loop state, but ``entities``
+ costs one indexed DB lookup, so the whole record is only built on request.
+ """
+
+ entities: Optional[List[ProvenanceEntity]] = None
+
+ failed_sources: Optional[List[str]] = None
+
+ sources: Optional[List[ProvenanceSource]] = None
+
+ steps: Optional[List[ProvenanceStep]] = None
diff --git a/src/hyperspell/types/shared/provenance_entity.py b/src/hyperspell/types/shared/provenance_entity.py
new file mode 100644
index 00000000..44565575
--- /dev/null
+++ b/src/hyperspell/types/shared/provenance_entity.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["ProvenanceEntity"]
+
+
+class ProvenanceEntity(BaseModel):
+ """A canonical entity referenced by the answer's source documents."""
+
+ id: str
+
+ name: str
+
+ type: str
diff --git a/src/hyperspell/types/shared/resource.py b/src/hyperspell/types/shared/provenance_source.py
similarity index 62%
rename from src/hyperspell/types/shared/resource.py
rename to src/hyperspell/types/shared/provenance_source.py
index ccc16b91..1be00cbb 100644
--- a/src/hyperspell/types/shared/resource.py
+++ b/src/hyperspell/types/shared/provenance_source.py
@@ -3,13 +3,14 @@
from typing import Optional
from typing_extensions import Literal
-from .metadata import Metadata
from ..._models import BaseModel
-__all__ = ["Resource"]
+__all__ = ["ProvenanceSource"]
-class Resource(BaseModel):
+class ProvenanceSource(BaseModel):
+ """A source document that informed the final answer (the post-rank result set)."""
+
resource_id: str
source: Literal[
@@ -27,17 +28,17 @@ class Resource(BaseModel):
"trace",
"microsoft_teams",
"gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
]
- folder_id: Optional[str] = None
- """Provider folder ID this resource belongs to"""
-
- metadata: Optional[Metadata] = None
-
- parent_folder_id: Optional[str] = None
- """Parent folder ID for policy inheritance"""
-
score: Optional[float] = None
- """The relevance of the resource to the query"""
title: Optional[str] = None
diff --git a/src/hyperspell/types/shared/provenance_step.py b/src/hyperspell/types/shared/provenance_step.py
new file mode 100644
index 00000000..1fb86b26
--- /dev/null
+++ b/src/hyperspell/types/shared/provenance_step.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["ProvenanceStep"]
+
+
+class ProvenanceStep(BaseModel):
+ """One tool invocation in the agent's search trajectory (audit trail)."""
+
+ iteration: int
+
+ status: str
+
+ tool: str
+
+ query: Optional[str] = None
+
+ result_count: Optional[int] = None
+
+ source: Optional[str] = None
diff --git a/src/hyperspell/types/shared/query_result.py b/src/hyperspell/types/shared/query_result.py
index 99efa3d0..23819e33 100644
--- a/src/hyperspell/types/shared/query_result.py
+++ b/src/hyperspell/types/shared/query_result.py
@@ -1,19 +1,25 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from __future__ import annotations
+
from typing import Dict, List, Optional
-from .resource import Resource
from ..._models import BaseModel
+from .provenance import Provenance
__all__ = ["QueryResult"]
class QueryResult(BaseModel):
- documents: List[Resource]
-
answer: Optional[str] = None
"""The answer to the query, if the request was set to answer."""
+ documents: Optional[List["ScoredDocumentResponse"]] = None
+ """
+ The matching documents, each carrying its hyperdoc tree plus query-path
+ score/highlights/summary (ENG-2479 Phase 4).
+ """
+
errors: Optional[List[Dict[str, str]]] = None
"""Errors that occurred during the query.
@@ -21,6 +27,17 @@ class QueryResult(BaseModel):
shown to the user.
"""
+ provenance: Optional[Provenance] = None
+ """Auditability record attached to an agentic answer.
+
+ Gated behind `provenance=true` on the request: the cheap parts (sources, steps,
+ failed_sources) are derived from in-memory loop state, but `entities` costs one
+ indexed DB lookup, so the whole record is only built on request.
+ """
+
+ query: Optional[str] = None
+ """The query string that was issued."""
+
query_id: Optional[str] = None
"""The ID of the query.
@@ -30,3 +47,6 @@ class QueryResult(BaseModel):
score: Optional[float] = None
"""The average score of the query feedback, if any."""
+
+
+from .scored_document_response import ScoredDocumentResponse
diff --git a/src/hyperspell/types/shared/quote.py b/src/hyperspell/types/shared/quote.py
new file mode 100644
index 00000000..8d59ffde
--- /dev/null
+++ b/src/hyperspell/types/shared/quote.py
@@ -0,0 +1,127 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Quote", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class Quote(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["quote"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/scored_document_response.py b/src/hyperspell/types/shared/scored_document_response.py
new file mode 100644
index 00000000..6e3902f1
--- /dev/null
+++ b/src/hyperspell/types/shared/scored_document_response.py
@@ -0,0 +1,120 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .trace import Trace
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+
+__all__ = ["ScoredDocumentResponse", "Document"]
+
+Document: TypeAlias = Annotated[
+ Union[
+ "document.Document",
+ "Website",
+ "Task",
+ "Person",
+ "Message",
+ "Event",
+ "File",
+ "Conversation",
+ Trace,
+ "Transcript",
+ "Company",
+ "Deal",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class ScoredDocumentResponse(BaseModel):
+ """
+ A `DocumentResponse` plus the query-path fields a `ScoredDocument` carries
+ (ENG-2479): relevance score, matched highlights, and the concatenated
+ summary of those highlights.
+ """
+
+ document: Document
+ """The full hyperdoc tree.
+
+ Switch on `type` for the document frame and recurse `children` for the body —
+ see the `` renderer.
+ """
+
+ resource_id: str
+
+ source: Literal[
+ "reddit",
+ "notion",
+ "slack",
+ "google_calendar",
+ "google_mail",
+ "box",
+ "dropbox",
+ "github",
+ "google_drive",
+ "vault",
+ "web_crawler",
+ "trace",
+ "microsoft_teams",
+ "gmail_actions",
+ "granola",
+ "fathom",
+ "fireflies",
+ "linear",
+ "hubspot",
+ "salesforce",
+ "coda",
+ "lightfield",
+ "gong",
+ ]
+
+ type: str
+ """Hyperdoc document type discriminator (document, message, file, event, ...)."""
+
+ collection: Optional[str] = None
+ """The document's collection, if any."""
+
+ document_date: Optional[datetime] = None
+ """The document's own date (e.g. email sent date, event date)."""
+
+ highlights: Optional[List[object]] = None
+ """The matched chunks that made this document a hit, with per-chunk scores."""
+
+ ingested_at: Optional[datetime] = None
+ """When Hyperspell first indexed the document."""
+
+ last_modified_at: Optional[datetime] = None
+ """When the source document was last modified."""
+
+ metadata: Optional[Dict[str, object]] = None
+ """Filterable custom metadata attached to the document."""
+
+ score: Optional[float] = None
+ """Relevance of the document to the query."""
+
+ status: Optional[Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"]] = None
+ """Indexing status of the document."""
+
+ summary: Optional[str] = None
+ """Concatenated text of the matched highlights."""
+
+ title: Optional[str] = None
+ """Human-readable document title."""
+
+
+from . import document
+from .deal import Deal
+from .file import File
+from .task import Task
+from .event import Event
+from .person import Person
+from .company import Company
+from .message import Message
+from .website import Website
+from .transcript import Transcript
+from .conversation import Conversation
diff --git a/src/hyperspell/types/shared/table.py b/src/hyperspell/types/shared/table.py
new file mode 100644
index 00000000..3d503bd6
--- /dev/null
+++ b/src/hyperspell/types/shared/table.py
@@ -0,0 +1,39 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Table"]
+
+
+class Table(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[List["TableRow"]] = None
+
+ has_header: Optional[bool] = None
+ """Whether the first row should be treated as a header"""
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["table"]] = None
+
+
+from .table_row import TableRow
diff --git a/src/hyperspell/types/shared/table_cell.py b/src/hyperspell/types/shared/table_cell.py
new file mode 100644
index 00000000..ca23c9ae
--- /dev/null
+++ b/src/hyperspell/types/shared/table_cell.py
@@ -0,0 +1,129 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["TableCell", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class TableCell(BaseModel):
+ id: Optional[str] = None
+
+ align: Optional[Literal["left", "center", "right"]] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["table_cell"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
diff --git a/src/hyperspell/types/shared/table_row.py b/src/hyperspell/types/shared/table_row.py
new file mode 100644
index 00000000..25b496d0
--- /dev/null
+++ b/src/hyperspell/types/shared/table_row.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["TableRow"]
+
+
+class TableRow(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[List["TableCell"]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["table_row"]] = None
+
+
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/task.py b/src/hyperspell/types/shared/task.py
new file mode 100644
index 00000000..4f03723e
--- /dev/null
+++ b/src/hyperspell/types/shared/task.py
@@ -0,0 +1,102 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Task", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Task(BaseModel):
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ comments: Optional[TypingList["Message"]] = None
+
+ due_at: Optional[datetime] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ priority: Optional[Literal["urgent", "high", "medium", "low"]] = None
+
+ status: Optional[Literal["completed", "not_started", "in_progress", "cancelled"]] = None
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["task"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .message import Message
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/text.py b/src/hyperspell/types/shared/text.py
new file mode 100644
index 00000000..1cd76e31
--- /dev/null
+++ b/src/hyperspell/types/shared/text.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Text"]
+
+
+class Text(BaseModel):
+ text: str
+
+ id: Optional[str] = None
+
+ marks: Optional[List[Literal["bold", "italic", "underline", "strikethrough", "code", "math"]]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["text"]] = None
diff --git a/src/hyperspell/types/shared/to_do.py b/src/hyperspell/types/shared/to_do.py
new file mode 100644
index 00000000..ae186b14
--- /dev/null
+++ b/src/hyperspell/types/shared/to_do.py
@@ -0,0 +1,129 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias, TypeAliasType
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._compat import PYDANTIC_V1
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["ToDo", "Child"]
+
+if TYPE_CHECKING or not PYDANTIC_V1:
+ Child = TypeAliasType(
+ "Child",
+ Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ],
+ )
+else:
+ Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+
+class ToDo(BaseModel):
+ id: Optional[str] = None
+
+ checked: Optional[bool] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ type: Optional[Literal["todo"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/src/hyperspell/types/shared/tool_call.py b/src/hyperspell/types/shared/tool_call.py
new file mode 100644
index 00000000..cf4908bc
--- /dev/null
+++ b/src/hyperspell/types/shared/tool_call.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["ToolCall"]
+
+
+class ToolCall(BaseModel):
+ """A tool/function call made by the assistant."""
+
+ tool_call_id: str
+
+ tool_name: str
+
+ id: Optional[str] = None
+
+ args: Optional[Dict[str, object]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["tool_call"]] = None
diff --git a/src/hyperspell/types/shared/tool_result.py b/src/hyperspell/types/shared/tool_result.py
new file mode 100644
index 00000000..661ab774
--- /dev/null
+++ b/src/hyperspell/types/shared/tool_result.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Union, Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["ToolResult"]
+
+
+class ToolResult(BaseModel):
+ """The result of a tool call."""
+
+ output: Union[str, Dict[str, object], List[object]]
+
+ tool_call_id: str
+
+ tool_name: str
+
+ id: Optional[str] = None
+
+ is_error: Optional[bool] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ type: Optional[Literal["tool_result"]] = None
diff --git a/src/hyperspell/types/shared/trace.py b/src/hyperspell/types/shared/trace.py
new file mode 100644
index 00000000..1c47c5cc
--- /dev/null
+++ b/src/hyperspell/types/shared/trace.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Trace", "Child"]
+
+Child: TypeAlias = Annotated[Union[TraceMessage, ToolCall, ToolResult], PropertyInfo(discriminator="type")]
+
+
+class Trace(BaseModel):
+ """An agent trace/transcript containing a sequence of steps.
+
+ Steps can be TraceMessage (user/assistant messages or thinking),
+ ToolCall (function calls), or ToolResult (tool responses).
+ """
+
+ id: Optional[str] = None
+
+ children: Optional[List[Child]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["trace"]] = None
diff --git a/src/hyperspell/types/shared/trace_message.py b/src/hyperspell/types/shared/trace_message.py
new file mode 100644
index 00000000..72645fdf
--- /dev/null
+++ b/src/hyperspell/types/shared/trace_message.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["TraceMessage"]
+
+
+class TraceMessage(BaseModel):
+ """A message in an agent trace (user message, assistant message, or thinking)."""
+
+ text: str
+
+ id: Optional[str] = None
+
+ message_type: Optional[Literal["message", "thinking"]] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ role: Optional[Literal["user", "assistant"]] = None
+
+ timestamp: Optional[datetime] = None
+
+ type: Optional[Literal["trace_message"]] = None
diff --git a/src/hyperspell/types/shared/transcript.py b/src/hyperspell/types/shared/transcript.py
new file mode 100644
index 00000000..faab4b66
--- /dev/null
+++ b/src/hyperspell/types/shared/transcript.py
@@ -0,0 +1,54 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Transcript"]
+
+
+class Transcript(BaseModel):
+ """
+ A time-anchored, speaker-attributed transcript — meetings, calls
+ (ENG-2476/D10; mirrors the Trace+TraceStep precedent).
+
+ Utterance timestamps are relative offsets from `started_at`, which is the
+ absolute wall-clock anchor.
+ """
+
+ id: Optional[str] = None
+
+ children: Optional[List["Utterance"]] = None
+
+ ended_at: Optional[datetime] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ participants: Optional[List["Person"]] = None
+
+ started_at: Optional[datetime] = None
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["transcript"]] = None
+
+
+from .person import Person
+from .utterance import Utterance
diff --git a/src/hyperspell/types/shared/utterance.py b/src/hyperspell/types/shared/utterance.py
new file mode 100644
index 00000000..db249782
--- /dev/null
+++ b/src/hyperspell/types/shared/utterance.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .metadata import Metadata
+from ..._models import BaseModel
+
+__all__ = ["Utterance"]
+
+
+class Utterance(BaseModel):
+ """A speaker-attributed segment of a transcript (ENG-2476/D10).
+
+ "Utterance" is the standard name for this across transcription providers
+ (AssemblyAI, Deepgram, Rev). Timestamps are relative offsets in seconds —
+ provider-native; absolute times derive from `Transcript.started_at`.
+ """
+
+ text: str
+
+ id: Optional[str] = None
+
+ end: Optional[float] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ speaker: Optional["Person"] = None
+
+ start: Optional[float] = None
+
+ type: Optional[Literal["utterance"]] = None
+
+
+from .person import Person
diff --git a/src/hyperspell/types/shared/website.py b/src/hyperspell/types/shared/website.py
new file mode 100644
index 00000000..d0a5b49a
--- /dev/null
+++ b/src/hyperspell/types/shared/website.py
@@ -0,0 +1,104 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List as TypingList, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .blob import Blob
+from .code import Code
+from .link import Link
+from .text import Text
+from .image import Image
+from .comment import Comment
+from .divider import Divider
+from ..._utils import PropertyInfo
+from .metadata import Metadata
+from ..._models import BaseModel
+from .tool_call import ToolCall
+from .line_break import LineBreak
+from .tool_result import ToolResult
+from .trace_message import TraceMessage
+
+__all__ = ["Website", "Child"]
+
+Child: TypeAlias = Annotated[
+ Union[
+ Blob,
+ "Callout",
+ "Chunk",
+ Code,
+ Comment,
+ Divider,
+ "Equation",
+ "Footnote",
+ "Heading",
+ Image,
+ Link,
+ LineBreak,
+ "ListList",
+ "ListItem",
+ "Paragraph",
+ "Quote",
+ "Table",
+ "TableCell",
+ "TableRow",
+ Text,
+ "ToDo",
+ ToolCall,
+ ToolResult,
+ TraceMessage,
+ "Utterance",
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
+class Website(BaseModel):
+ url: str
+
+ id: Optional[str] = None
+
+ children: Optional[TypingList[Child]] = None
+
+ description: Optional[str] = None
+
+ favicon: Optional[str] = None
+
+ image_url: Optional[str] = None
+
+ language: Optional[str] = None
+
+ metadata: Optional[Metadata] = None
+ """Per-block annotations carried by any Hyperdoc node (ENG-1390).
+
+ Out-of-band annotations that travel with a block but aren't part of its content:
+ provenance (`sources`) and human edit attribution (`edited_by`). New annotation
+ types get added here as typed fields as the need arises.
+
+ Empty by default. Because `Node.model_dump` forces `exclude_none=True`, an unset
+ `metadata` (None) is dropped from serialization entirely, and within a populated
+ `Metadata` only the set keys survive.
+ """
+
+ text: Optional[str] = None
+
+ title: Optional[str] = None
+
+ type: Optional[Literal["website"]] = None
+
+
+from .list import List as ListList
+from .chunk import Chunk
+from .quote import Quote
+from .table import Table
+from .to_do import ToDo
+from .callout import Callout
+from .heading import Heading
+from .equation import Equation
+from .footnote import Footnote
+from .list_item import ListItem
+from .paragraph import Paragraph
+from .table_row import TableRow
+from .utterance import Utterance
+from .table_cell import TableCell
diff --git a/tests/api_resources/test_evaluate.py b/tests/api_resources/test_evaluate.py
index c8ab9f33..6a5cc524 100644
--- a/tests/api_resources/test_evaluate.py
+++ b/tests/api_resources/test_evaluate.py
@@ -11,8 +11,10 @@
from tests.utils import assert_matches_type
from hyperspell.types import (
EvaluateScoreQueryResponse,
+ EvaluateListQueriesResponse,
EvaluateScoreHighlightResponse,
)
+from hyperspell.pagination import SyncCursorPage, AsyncCursorPage
from hyperspell.types.shared import QueryResult
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -59,6 +61,40 @@ def test_path_params_get_query(self, client: Hyperspell) -> None:
"",
)
+ @parametrize
+ def test_method_list_queries(self, client: Hyperspell) -> None:
+ evaluate = client.evaluate.list_queries()
+ assert_matches_type(SyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ def test_method_list_queries_with_all_params(self, client: Hyperspell) -> None:
+ evaluate = client.evaluate.list_queries(
+ cursor="cursor",
+ size=0,
+ user_id="user_id",
+ )
+ assert_matches_type(SyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ def test_raw_response_list_queries(self, client: Hyperspell) -> None:
+ response = client.evaluate.with_raw_response.list_queries()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ evaluate = response.parse()
+ assert_matches_type(SyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list_queries(self, client: Hyperspell) -> None:
+ with client.evaluate.with_streaming_response.list_queries() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ evaluate = response.parse()
+ assert_matches_type(SyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_score_highlight(self, client: Hyperspell) -> None:
evaluate = client.evaluate.score_highlight(
@@ -196,6 +232,40 @@ async def test_path_params_get_query(self, async_client: AsyncHyperspell) -> Non
"",
)
+ @parametrize
+ async def test_method_list_queries(self, async_client: AsyncHyperspell) -> None:
+ evaluate = await async_client.evaluate.list_queries()
+ assert_matches_type(AsyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ async def test_method_list_queries_with_all_params(self, async_client: AsyncHyperspell) -> None:
+ evaluate = await async_client.evaluate.list_queries(
+ cursor="cursor",
+ size=0,
+ user_id="user_id",
+ )
+ assert_matches_type(AsyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list_queries(self, async_client: AsyncHyperspell) -> None:
+ response = await async_client.evaluate.with_raw_response.list_queries()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ evaluate = await response.parse()
+ assert_matches_type(AsyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list_queries(self, async_client: AsyncHyperspell) -> None:
+ async with async_client.evaluate.with_streaming_response.list_queries() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ evaluate = await response.parse()
+ assert_matches_type(AsyncCursorPage[EvaluateListQueriesResponse], evaluate, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_score_highlight(self, async_client: AsyncHyperspell) -> None:
evaluate = await async_client.evaluate.score_highlight(
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 2a983b3f..554fc4b1 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -10,15 +10,16 @@
from hyperspell import Hyperspell, AsyncHyperspell
from tests.utils import assert_matches_type
from hyperspell.types import (
- Memory,
MemoryStatus,
+ MemoryGetResponse,
+ MemoryListResponse,
MemoryDeleteResponse,
MemoryStatusResponse,
MemoryAddBulkResponse,
)
from hyperspell._utils import parse_datetime
from hyperspell.pagination import SyncCursorPage, AsyncCursorPage
-from hyperspell.types.shared import Resource, QueryResult
+from hyperspell.types.shared import QueryResult
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -40,6 +41,7 @@ def test_method_update_with_all_params(self, client: Hyperspell) -> None:
resource_id="resource_id",
source="reddit",
collection="string",
+ date=parse_datetime("2019-12-27T18:11:19.117Z"),
metadata={"foo": "string"},
text="string",
title="string",
@@ -83,7 +85,7 @@ def test_path_params_update(self, client: Hyperspell) -> None:
@parametrize
def test_method_list(self, client: Hyperspell) -> None:
memory = client.memories.list()
- assert_matches_type(SyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Hyperspell) -> None:
@@ -95,7 +97,7 @@ def test_method_list_with_all_params(self, client: Hyperspell) -> None:
source="reddit",
status="pending",
)
- assert_matches_type(SyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_raw_response_list(self, client: Hyperspell) -> None:
@@ -104,7 +106,7 @@ def test_raw_response_list(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_streaming_response_list(self, client: Hyperspell) -> None:
@@ -113,7 +115,7 @@ def test_streaming_response_list(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -243,7 +245,7 @@ def test_method_get(self, client: Hyperspell) -> None:
resource_id="resource_id",
source="reddit",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_raw_response_get(self, client: Hyperspell) -> None:
@@ -255,7 +257,7 @@ def test_raw_response_get(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_streaming_response_get(self, client: Hyperspell) -> None:
@@ -267,7 +269,7 @@ def test_streaming_response_get(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -291,7 +293,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
memory = client.memories.search(
query="What does Hyperspell do?",
answer=True,
- effort=0,
+ effort="minimal",
max_results=0,
options={
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
@@ -314,12 +316,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"notion_page_ids": ["string"],
"weight": 0,
},
- "reddit": {
- "period": "hour",
- "sort": "relevance",
- "subreddit": "subreddit",
- "weight": 0,
- },
+ "recency_half_life_days": 1,
"resource_ids": ["string"],
"slack": {
"channels": ["string"],
@@ -336,6 +333,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"weight": 0,
},
},
+ provenance=True,
sources=["vault"],
)
assert_matches_type(QueryResult, memory, path=["response"])
@@ -449,6 +447,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncHyperspell
resource_id="resource_id",
source="reddit",
collection="string",
+ date=parse_datetime("2019-12-27T18:11:19.117Z"),
metadata={"foo": "string"},
text="string",
title="string",
@@ -492,7 +491,7 @@ async def test_path_params_update(self, async_client: AsyncHyperspell) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.list()
- assert_matches_type(AsyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None:
@@ -504,7 +503,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncHyperspell)
source="reddit",
status="pending",
)
- assert_matches_type(AsyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -513,7 +512,7 @@ async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -522,7 +521,7 @@ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Resource], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -652,7 +651,7 @@ async def test_method_get(self, async_client: AsyncHyperspell) -> None:
resource_id="resource_id",
source="reddit",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -664,7 +663,7 @@ async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -676,7 +675,7 @@ async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> No
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -700,7 +699,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
memory = await async_client.memories.search(
query="What does Hyperspell do?",
answer=True,
- effort=0,
+ effort="minimal",
max_results=0,
options={
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
@@ -723,12 +722,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"notion_page_ids": ["string"],
"weight": 0,
},
- "reddit": {
- "period": "hour",
- "sort": "relevance",
- "subreddit": "subreddit",
- "weight": 0,
- },
+ "recency_half_life_days": 1,
"resource_ids": ["string"],
"slack": {
"channels": ["string"],
@@ -745,6 +739,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"weight": 0,
},
},
+ provenance=True,
sources=["vault"],
)
assert_matches_type(QueryResult, memory, path=["response"])
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
deleted file mode 100644
index 93ca9c04..00000000
--- a/tests/test_deepcopy.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from hyperspell._utils import deepcopy_minimal
-
-
-def assert_different_identities(obj1: object, obj2: object) -> None:
- assert obj1 == obj2
- assert id(obj1) != id(obj2)
-
-
-def test_simple_dict() -> None:
- obj1 = {"foo": "bar"}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_dict() -> None:
- obj1 = {"foo": {"bar": True}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
-
-
-def test_complex_nested_dict() -> None:
- obj1 = {"foo": {"bar": [{"hello": "world"}]}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
- assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"])
- assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0])
-
-
-def test_simple_list() -> None:
- obj1 = ["a", "b", "c"]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_list() -> None:
- obj1 = ["a", [1, 2, 3]]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1[1], obj2[1])
-
-
-class MyObject: ...
-
-
-def test_ignores_other_types() -> None:
- # custom classes
- my_obj = MyObject()
- obj1 = {"foo": my_obj}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert obj1["foo"] is my_obj
-
- # tuples
- obj3 = ("a", "b")
- obj4 = deepcopy_minimal(obj3)
- assert obj3 is obj4
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index da9fb3d5..e17802c5 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -4,7 +4,7 @@
import pytest
-from hyperspell._types import FileTypes
+from hyperspell._types import FileTypes, ArrayFormat
from hyperspell._utils import extract_files
@@ -37,10 +37,7 @@ def test_multiple_files() -> None:
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [
- ("files[]", b"file one"),
- ("files[]", b"file two"),
- ]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
@@ -71,3 +68,24 @@ def test_ignores_incorrect_paths(
expected: list[tuple[str, FileTypes]],
) -> None:
assert extract_files(query, paths=paths) == expected
+
+
+@pytest.mark.parametrize(
+ "array_format,expected_top_level,expected_nested",
+ [
+ ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]),
+ ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]),
+ ],
+)
+def test_array_format_controls_file_field_names(
+ array_format: ArrayFormat,
+ expected_top_level: list[tuple[str, FileTypes]],
+ expected_nested: list[tuple[str, FileTypes]],
+) -> None:
+ top_level = {"files": [b"a", b"b"]}
+ assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level
+
+ nested = {"items": [{"file": b"a"}, {"file": b"b"}]}
+ assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested
diff --git a/tests/test_files.py b/tests/test_files.py
index aae00cb3..aded237c 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -4,7 +4,8 @@
import pytest
from dirty_equals import IsDict, IsList, IsBytes, IsTuple
-from hyperspell._files import to_httpx_files, async_to_httpx_files
+from hyperspell._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
+from hyperspell._utils import extract_files
readme_path = Path(__file__).parent.parent.joinpath("README.md")
@@ -49,3 +50,99 @@ def test_string_not_allowed() -> None:
"file": "foo", # type: ignore
}
)
+
+
+def assert_different_identities(obj1: object, obj2: object) -> None:
+ assert obj1 == obj2
+ assert obj1 is not obj2
+
+
+class TestDeepcopyWithPaths:
+ def test_copies_top_level_dict(self) -> None:
+ original = {"file": b"data", "other": "value"}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+
+ def test_file_value_is_same_reference(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+ assert result["file"] is file_bytes
+
+ def test_list_popped_wholesale(self) -> None:
+ files = [b"f1", b"f2"]
+ original = {"files": files, "title": "t"}
+ result = deepcopy_with_paths(original, [["files", ""]])
+ assert_different_identities(result, original)
+ result_files = result["files"]
+ assert isinstance(result_files, list)
+ assert_different_identities(result_files, files)
+
+ def test_nested_array_path_copies_list_and_elements(self) -> None:
+ elem1 = {"file": b"f1", "extra": 1}
+ elem2 = {"file": b"f2", "extra": 2}
+ original = {"items": [elem1, elem2]}
+ result = deepcopy_with_paths(original, [["items", "", "file"]])
+ assert_different_identities(result, original)
+ result_items = result["items"]
+ assert isinstance(result_items, list)
+ assert_different_identities(result_items, original["items"])
+ assert_different_identities(result_items[0], elem1)
+ assert_different_identities(result_items[1], elem2)
+
+ def test_empty_paths_returns_same_object(self) -> None:
+ original = {"foo": "bar"}
+ result = deepcopy_with_paths(original, [])
+ assert result is original
+
+ def test_multiple_paths(self) -> None:
+ f1 = b"file1"
+ f2 = b"file2"
+ original = {"a": f1, "b": f2, "c": "unchanged"}
+ result = deepcopy_with_paths(original, [["a"], ["b"]])
+ assert_different_identities(result, original)
+ assert result["a"] is f1
+ assert result["b"] is f2
+ assert result["c"] is original["c"]
+
+ def test_extract_files_does_not_mutate_original_top_level(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes, "other": "value"}
+
+ copied = deepcopy_with_paths(original, [["file"]])
+ extracted = extract_files(copied, paths=[["file"]])
+
+ assert extracted == [("file", file_bytes)]
+ assert original == {"file": file_bytes, "other": "value"}
+ assert copied == {"other": "value"}
+
+ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
+ file1 = b"f1"
+ file2 = b"f2"
+ original = {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+
+ copied = deepcopy_with_paths(original, [["items", "", "file"]])
+ extracted = extract_files(copied, paths=[["items", "", "file"]])
+
+ assert [entry for _, entry in extracted] == [file1, file2]
+ assert original == {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+ assert copied == {
+ "items": [
+ {"extra": 1},
+ {"extra": 2},
+ ],
+ "title": "example",
+ }
diff --git a/tests/test_models.py b/tests/test_models.py
index 88942a41..84a13569 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,8 @@
import json
-from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated, TypeAliasType
+from collections import deque
+from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
import pytest
import pydantic
@@ -9,7 +10,7 @@
from hyperspell._utils import PropertyInfo
from hyperspell._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from hyperspell._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
+from hyperspell._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ...
assert model.a.prop == 1
assert isinstance(model.a, Item)
assert model.other == "foo"
+
+
+# NOTE: Workaround for Pydantic Iterable behavior.
+# Iterable fields are replaced with a ValidatorIterator and may be consumed
+# during serialization, which can cause subsequent dumps to return empty data.
+# See: https://github.com/pydantic/pydantic/issues/9541
+@pytest.mark.parametrize(
+ "data, expected_validated",
+ [
+ ([1, 2, 3], [1, 2, 3]),
+ ((1, 2, 3), (1, 2, 3)),
+ (set([1, 2, 3]), set([1, 2, 3])),
+ (iter([1, 2, 3]), [1, 2, 3]),
+ ([], []),
+ ((x for x in [1, 2, 3]), [1, 2, 3]),
+ (map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
+ (frozenset([1, 2, 3]), frozenset([1, 2, 3])),
+ (deque([1, 2, 3]), deque([1, 2, 3])),
+ ],
+ ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
+)
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[int]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": data}})
+ assert m.data["items"] == expected_validated
+
+ # Verify repeated dumps don't lose data (the original bug)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+
+
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction_str_falls_back_to_list() -> None:
+ # str is iterable (over chars), but str(list_of_chars) produces the list's repr
+ # rather than reconstructing a string from items. We special-case str to fall
+ # back to list instead of attempting reconstruction.
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[str]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": "hello"}})
+
+ # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
+ assert m.data["items"] == ["h", "e", "l", "l", "o"]
+ assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]