surfacedocs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- surfacedocs-0.1.0/.gitignore +79 -0
- surfacedocs-0.1.0/AGENTS/CLIENT_TASK.md +277 -0
- surfacedocs-0.1.0/AGENTS/DOCS_TASK.md +345 -0
- surfacedocs-0.1.0/AGENTS/E2E_TEST_TASK.md +188 -0
- surfacedocs-0.1.0/AGENTS/LLM_QUICKSTARTS_TASK.md +214 -0
- surfacedocs-0.1.0/AGENTS/ORCHESTRATION.md +262 -0
- surfacedocs-0.1.0/AGENTS/PROD_QUICKSTART_TASK.md +127 -0
- surfacedocs-0.1.0/AGENTS/PROMPT_TASK.md +109 -0
- surfacedocs-0.1.0/AGENTS/PUBLISH_TASK.md +239 -0
- surfacedocs-0.1.0/AGENTS/QUICKSTART_EXAMPLE_TASK.md +66 -0
- surfacedocs-0.1.0/AGENTS/SCHEMA_EXAMPLES_TASK.md +186 -0
- surfacedocs-0.1.0/AGENTS/SCHEMA_TASK.md +122 -0
- surfacedocs-0.1.0/AGENTS/SETUP_TASK.md +197 -0
- surfacedocs-0.1.0/AGENTS/TESTS_TASK.md +385 -0
- surfacedocs-0.1.0/LICENSE +21 -0
- surfacedocs-0.1.0/PKG-INFO +302 -0
- surfacedocs-0.1.0/README.md +275 -0
- surfacedocs-0.1.0/pyproject.toml +44 -0
- surfacedocs-0.1.0/src/surfacedocs/__init__.py +27 -0
- surfacedocs-0.1.0/src/surfacedocs/client.py +185 -0
- surfacedocs-0.1.0/src/surfacedocs/exceptions.py +25 -0
- surfacedocs-0.1.0/src/surfacedocs/prompt.py +55 -0
- surfacedocs-0.1.0/src/surfacedocs/schema.py +222 -0
- surfacedocs-0.1.0/tests/__init__.py +1 -0
- surfacedocs-0.1.0/tests/conftest.py +44 -0
- surfacedocs-0.1.0/tests/test_client.py +457 -0
- surfacedocs-0.1.0/tests/test_exceptions.py +162 -0
- surfacedocs-0.1.0/tests/test_init.py +146 -0
- surfacedocs-0.1.0/tests/test_prompt.py +156 -0
- surfacedocs-0.1.0/tests/test_schema.py +222 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.venv
|
|
56
|
+
env/
|
|
57
|
+
venv/
|
|
58
|
+
ENV/
|
|
59
|
+
env.bak/
|
|
60
|
+
venv.bak/
|
|
61
|
+
|
|
62
|
+
# IDE
|
|
63
|
+
.idea/
|
|
64
|
+
.vscode/
|
|
65
|
+
*.swp
|
|
66
|
+
*.swo
|
|
67
|
+
*~
|
|
68
|
+
|
|
69
|
+
# mypy
|
|
70
|
+
.mypy_cache/
|
|
71
|
+
.dmypy.json
|
|
72
|
+
dmypy.json
|
|
73
|
+
|
|
74
|
+
# ruff
|
|
75
|
+
.ruff_cache/
|
|
76
|
+
|
|
77
|
+
# OS
|
|
78
|
+
.DS_Store
|
|
79
|
+
Thumbs.db
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Task: HTTP Client Implementation
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE
|
|
4
|
+
|
|
5
|
+
## Objective
|
|
6
|
+
|
|
7
|
+
Implement the `SurfaceDocs` client class that saves documents to the ingress API.
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
- SETUP_TASK, SCHEMA_TASK, PROMPT_TASK complete
|
|
12
|
+
- Client uses httpx for HTTP requests
|
|
13
|
+
- Must call `POST /v1/documents` on ingress-api
|
|
14
|
+
- Reference: `plans/python-sdk-spec.md`, `services/ingress-api/src/main.py`
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Deliverable
|
|
19
|
+
|
|
20
|
+
Update `src/surfacedocs/client.py` with the complete `SurfaceDocs` class.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## API Endpoint
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
POST /v1/documents
|
|
28
|
+
Host: ingress.surfacedocs.dev (prod) or ingress.dev.surfacedocs.dev (dev)
|
|
29
|
+
Authorization: Bearer <api_key>
|
|
30
|
+
Content-Type: application/json
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
"title": "Document title",
|
|
34
|
+
"folder_id": "optional_folder_id",
|
|
35
|
+
"content_type": "markdown",
|
|
36
|
+
"metadata": {"source": "agent-name"},
|
|
37
|
+
"blocks": [...]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Response 201:
|
|
41
|
+
{
|
|
42
|
+
"id": "doc_abc123",
|
|
43
|
+
"url": "https://app.surfacedocs.dev/d/doc_abc123",
|
|
44
|
+
"folder_id": "folder_xyz",
|
|
45
|
+
"title": "Document title",
|
|
46
|
+
"block_count": 5,
|
|
47
|
+
"created_at": "2024-01-01T00:00:00Z"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Implementation
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import json
|
|
59
|
+
import os
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from typing import Any
|
|
62
|
+
|
|
63
|
+
import httpx
|
|
64
|
+
|
|
65
|
+
from surfacedocs.exceptions import (
|
|
66
|
+
AuthenticationError,
|
|
67
|
+
FolderNotFoundError,
|
|
68
|
+
SurfaceDocsError,
|
|
69
|
+
ValidationError,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class SaveResult:
|
|
75
|
+
"""Result from saving a document."""
|
|
76
|
+
id: str
|
|
77
|
+
url: str
|
|
78
|
+
folder_id: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SurfaceDocs:
|
|
82
|
+
"""SurfaceDocs API client."""
|
|
83
|
+
|
|
84
|
+
PROD_URL = "https://ingress.surfacedocs.dev"
|
|
85
|
+
DEV_URL = "https://ingress.dev.surfacedocs.dev"
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
api_key: str | None = None,
|
|
90
|
+
base_url: str | None = None,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Initialize the SurfaceDocs client.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
api_key: API key (sd_live_xxx or sd_test_xxx).
|
|
97
|
+
Falls back to SURFACEDOCS_API_KEY env var.
|
|
98
|
+
base_url: Override API URL. Auto-detected from key prefix if not set.
|
|
99
|
+
"""
|
|
100
|
+
self.api_key = api_key or os.environ.get("SURFACEDOCS_API_KEY")
|
|
101
|
+
if not self.api_key:
|
|
102
|
+
raise AuthenticationError("API key required. Pass api_key or set SURFACEDOCS_API_KEY.")
|
|
103
|
+
|
|
104
|
+
if base_url:
|
|
105
|
+
self.base_url = base_url.rstrip("/")
|
|
106
|
+
else:
|
|
107
|
+
self.base_url = self._detect_base_url(self.api_key)
|
|
108
|
+
|
|
109
|
+
self._client = httpx.Client(
|
|
110
|
+
base_url=self.base_url,
|
|
111
|
+
headers={
|
|
112
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
},
|
|
115
|
+
timeout=30.0,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _detect_base_url(self, api_key: str) -> str:
|
|
119
|
+
"""Detect base URL from API key prefix."""
|
|
120
|
+
if api_key.startswith("sd_test_"):
|
|
121
|
+
return self.DEV_URL
|
|
122
|
+
return self.PROD_URL
|
|
123
|
+
|
|
124
|
+
def save(
|
|
125
|
+
self,
|
|
126
|
+
content: str | dict[str, Any],
|
|
127
|
+
folder_id: str | None = None,
|
|
128
|
+
) -> SaveResult:
|
|
129
|
+
"""
|
|
130
|
+
Save a document from LLM output.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
content: JSON string or dict from LLM response
|
|
134
|
+
folder_id: Target folder (defaults to user's root folder)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
SaveResult with document ID and URL
|
|
138
|
+
"""
|
|
139
|
+
# Parse content if string
|
|
140
|
+
if isinstance(content, str):
|
|
141
|
+
try:
|
|
142
|
+
data = json.loads(content)
|
|
143
|
+
except json.JSONDecodeError as e:
|
|
144
|
+
raise ValidationError(f"Invalid JSON: {e}")
|
|
145
|
+
else:
|
|
146
|
+
data = content
|
|
147
|
+
|
|
148
|
+
# Extract fields
|
|
149
|
+
title = data.get("title")
|
|
150
|
+
blocks = data.get("blocks")
|
|
151
|
+
metadata = data.get("metadata")
|
|
152
|
+
|
|
153
|
+
if not title:
|
|
154
|
+
raise ValidationError("Document must have a title")
|
|
155
|
+
if not blocks:
|
|
156
|
+
raise ValidationError("Document must have blocks")
|
|
157
|
+
|
|
158
|
+
return self.save_raw(
|
|
159
|
+
title=title,
|
|
160
|
+
blocks=blocks,
|
|
161
|
+
folder_id=folder_id,
|
|
162
|
+
metadata=metadata,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def save_raw(
|
|
166
|
+
self,
|
|
167
|
+
title: str,
|
|
168
|
+
blocks: list[dict[str, Any]],
|
|
169
|
+
folder_id: str | None = None,
|
|
170
|
+
metadata: dict[str, Any] | None = None,
|
|
171
|
+
) -> SaveResult:
|
|
172
|
+
"""
|
|
173
|
+
Save a document with explicit parameters.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
title: Document title
|
|
177
|
+
blocks: List of block dicts
|
|
178
|
+
folder_id: Target folder (defaults to user's root folder)
|
|
179
|
+
metadata: Optional metadata dict
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
SaveResult with document ID and URL
|
|
183
|
+
"""
|
|
184
|
+
payload: dict[str, Any] = {
|
|
185
|
+
"title": title,
|
|
186
|
+
"blocks": blocks,
|
|
187
|
+
"content_type": "markdown",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if folder_id:
|
|
191
|
+
payload["folder_id"] = folder_id
|
|
192
|
+
if metadata:
|
|
193
|
+
payload["metadata"] = metadata
|
|
194
|
+
|
|
195
|
+
response = self._client.post("/v1/documents", json=payload)
|
|
196
|
+
return self._handle_response(response)
|
|
197
|
+
|
|
198
|
+
def _handle_response(self, response: httpx.Response) -> SaveResult:
|
|
199
|
+
"""Handle API response and map errors."""
|
|
200
|
+
if response.status_code == 201:
|
|
201
|
+
data = response.json()
|
|
202
|
+
return SaveResult(
|
|
203
|
+
id=data["id"],
|
|
204
|
+
url=data["url"],
|
|
205
|
+
folder_id=data["folder_id"],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Error handling
|
|
209
|
+
try:
|
|
210
|
+
error_data = response.json()
|
|
211
|
+
detail = error_data.get("detail", str(error_data))
|
|
212
|
+
except Exception:
|
|
213
|
+
detail = response.text or f"HTTP {response.status_code}"
|
|
214
|
+
|
|
215
|
+
if response.status_code == 401:
|
|
216
|
+
raise AuthenticationError(f"Authentication failed: {detail}")
|
|
217
|
+
elif response.status_code == 403:
|
|
218
|
+
raise AuthenticationError(f"Access denied: {detail}")
|
|
219
|
+
elif response.status_code == 404:
|
|
220
|
+
raise FolderNotFoundError(f"Folder not found: {detail}")
|
|
221
|
+
elif response.status_code == 422:
|
|
222
|
+
raise ValidationError(f"Validation error: {detail}")
|
|
223
|
+
else:
|
|
224
|
+
raise SurfaceDocsError(f"API error ({response.status_code}): {detail}")
|
|
225
|
+
|
|
226
|
+
def close(self) -> None:
|
|
227
|
+
"""Close the HTTP client."""
|
|
228
|
+
self._client.close()
|
|
229
|
+
|
|
230
|
+
def __enter__(self) -> SurfaceDocs:
|
|
231
|
+
return self
|
|
232
|
+
|
|
233
|
+
def __exit__(self, *args: Any) -> None:
|
|
234
|
+
self.close()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Verification
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from surfacedocs import SurfaceDocs, SurfaceDocsError
|
|
243
|
+
|
|
244
|
+
# Should raise without API key
|
|
245
|
+
try:
|
|
246
|
+
client = SurfaceDocs()
|
|
247
|
+
except SurfaceDocsError:
|
|
248
|
+
print("Correctly raised error without API key")
|
|
249
|
+
|
|
250
|
+
# Should detect dev URL
|
|
251
|
+
import os
|
|
252
|
+
os.environ["SURFACEDOCS_API_KEY"] = "sd_test_fake"
|
|
253
|
+
client = SurfaceDocs()
|
|
254
|
+
assert client.base_url == "https://ingress.dev.surfacedocs.dev"
|
|
255
|
+
|
|
256
|
+
# Should detect prod URL
|
|
257
|
+
client = SurfaceDocs(api_key="sd_live_fake")
|
|
258
|
+
assert client.base_url == "https://ingress.surfacedocs.dev"
|
|
259
|
+
|
|
260
|
+
# Context manager should work
|
|
261
|
+
with SurfaceDocs(api_key="sd_test_fake") as client:
|
|
262
|
+
assert client.api_key == "sd_test_fake"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Definition of Done
|
|
268
|
+
|
|
269
|
+
- [x] `SurfaceDocs` class implemented in `client.py`
|
|
270
|
+
- [x] `SaveResult` dataclass implemented
|
|
271
|
+
- [x] `save()` method accepts string or dict
|
|
272
|
+
- [x] `save_raw()` method accepts explicit params
|
|
273
|
+
- [x] API key from constructor or env var
|
|
274
|
+
- [x] Base URL auto-detected from key prefix
|
|
275
|
+
- [x] Error responses mapped to exception types
|
|
276
|
+
- [x] Context manager support (`with` statement)
|
|
277
|
+
- [x] Import works: `from surfacedocs import SurfaceDocs`
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Task: Documentation
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE
|
|
4
|
+
|
|
5
|
+
## Objective
|
|
6
|
+
|
|
7
|
+
Write user-facing documentation: README with examples, docstrings, and LICENSE.
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
- All code and tests complete
|
|
12
|
+
- README is primary documentation (shown on PyPI)
|
|
13
|
+
- Reference: `plans/python-sdk-spec.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Deliverables
|
|
18
|
+
|
|
19
|
+
1. `README.md` - Full documentation with examples
|
|
20
|
+
2. Docstrings on all public APIs
|
|
21
|
+
3. `LICENSE` - MIT license (verify exists)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## README.md
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
# SurfaceDocs Python SDK
|
|
29
|
+
|
|
30
|
+
Save LLM-generated documents to [SurfaceDocs](https://surfacedocs.dev).
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install surfacedocs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from surfacedocs import SurfaceDocs, DOCUMENT_SCHEMA, SYSTEM_PROMPT
|
|
42
|
+
from openai import OpenAI
|
|
43
|
+
|
|
44
|
+
# Initialize clients
|
|
45
|
+
openai = OpenAI()
|
|
46
|
+
docs = SurfaceDocs(api_key="sd_live_...")
|
|
47
|
+
|
|
48
|
+
# Generate a document with your LLM
|
|
49
|
+
response = openai.chat.completions.create(
|
|
50
|
+
model="gpt-4o",
|
|
51
|
+
messages=[
|
|
52
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
53
|
+
{"role": "user", "content": "Document our REST API authentication flow"},
|
|
54
|
+
],
|
|
55
|
+
response_format={
|
|
56
|
+
"type": "json_schema",
|
|
57
|
+
"json_schema": {
|
|
58
|
+
"name": "surfacedocs_document",
|
|
59
|
+
"schema": DOCUMENT_SCHEMA,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Save to SurfaceDocs
|
|
65
|
+
result = docs.save(response.choices[0].message.content)
|
|
66
|
+
print(result.url) # https://app.surfacedocs.dev/d/abc123
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What's Included
|
|
70
|
+
|
|
71
|
+
The SDK provides three exports:
|
|
72
|
+
|
|
73
|
+
| Export | Type | Purpose |
|
|
74
|
+
|--------|------|---------|
|
|
75
|
+
| `DOCUMENT_SCHEMA` | dict | JSON schema for LLM structured output |
|
|
76
|
+
| `SYSTEM_PROMPT` | str | Instructions for LLM to generate documents |
|
|
77
|
+
| `SurfaceDocs` | class | HTTP client to save documents |
|
|
78
|
+
|
|
79
|
+
## API Reference
|
|
80
|
+
|
|
81
|
+
### SurfaceDocs
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from surfacedocs import SurfaceDocs
|
|
85
|
+
|
|
86
|
+
# Initialize with API key
|
|
87
|
+
client = SurfaceDocs(api_key="sd_live_...")
|
|
88
|
+
|
|
89
|
+
# Or use environment variable
|
|
90
|
+
# export SURFACEDOCS_API_KEY=sd_live_...
|
|
91
|
+
client = SurfaceDocs()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### save(content, folder_id=None)
|
|
95
|
+
|
|
96
|
+
Save a document from LLM output.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# From JSON string
|
|
100
|
+
result = client.save(response.choices[0].message.content)
|
|
101
|
+
|
|
102
|
+
# From dict
|
|
103
|
+
result = client.save({
|
|
104
|
+
"title": "My Document",
|
|
105
|
+
"blocks": [{"type": "paragraph", "content": "Hello world"}]
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# To specific folder
|
|
109
|
+
result = client.save(content, folder_id="folder_abc123")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### save_raw(title, blocks, folder_id=None, metadata=None)
|
|
113
|
+
|
|
114
|
+
Save a document with explicit parameters.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
result = client.save_raw(
|
|
118
|
+
title="API Documentation",
|
|
119
|
+
blocks=[
|
|
120
|
+
{"type": "heading", "content": "Authentication", "metadata": {"level": 1}},
|
|
121
|
+
{"type": "paragraph", "content": "Use Bearer tokens for auth."},
|
|
122
|
+
{"type": "code", "content": "curl -H 'Authorization: Bearer ...'", "metadata": {"language": "bash"}},
|
|
123
|
+
],
|
|
124
|
+
metadata={"source": "doc-generator", "version": "1.0"},
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### SaveResult
|
|
129
|
+
|
|
130
|
+
Both methods return a `SaveResult`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
result.id # "doc_abc123"
|
|
134
|
+
result.url # "https://app.surfacedocs.dev/d/doc_abc123"
|
|
135
|
+
result.folder_id # "folder_xyz"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### DOCUMENT_SCHEMA
|
|
139
|
+
|
|
140
|
+
JSON schema dict for LLM structured output. Pass directly to your LLM provider.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from surfacedocs import DOCUMENT_SCHEMA
|
|
144
|
+
|
|
145
|
+
# OpenAI
|
|
146
|
+
response = openai.chat.completions.create(
|
|
147
|
+
model="gpt-4o",
|
|
148
|
+
messages=[...],
|
|
149
|
+
response_format={
|
|
150
|
+
"type": "json_schema",
|
|
151
|
+
"json_schema": {
|
|
152
|
+
"name": "surfacedocs_document",
|
|
153
|
+
"schema": DOCUMENT_SCHEMA,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Anthropic
|
|
159
|
+
response = anthropic.messages.create(
|
|
160
|
+
model="claude-sonnet-4-20250514",
|
|
161
|
+
messages=[...],
|
|
162
|
+
tools=[{
|
|
163
|
+
"name": "create_document",
|
|
164
|
+
"description": "Create a structured document",
|
|
165
|
+
"input_schema": DOCUMENT_SCHEMA,
|
|
166
|
+
}],
|
|
167
|
+
tool_choice={"type": "tool", "name": "create_document"},
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### SYSTEM_PROMPT
|
|
172
|
+
|
|
173
|
+
System prompt string to instruct LLMs on document format.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from surfacedocs import SYSTEM_PROMPT
|
|
177
|
+
|
|
178
|
+
messages = [
|
|
179
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
180
|
+
{"role": "user", "content": "Document the login flow"},
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Block Types
|
|
185
|
+
|
|
186
|
+
Documents are composed of blocks:
|
|
187
|
+
|
|
188
|
+
| Type | Description | Metadata |
|
|
189
|
+
|------|-------------|----------|
|
|
190
|
+
| `heading` | Section header | `level` (1-6) |
|
|
191
|
+
| `paragraph` | Body text | - |
|
|
192
|
+
| `code` | Code block | `language` (optional) |
|
|
193
|
+
| `list` | Bullet/numbered list | `listType` ("bullet" or "ordered") |
|
|
194
|
+
| `quote` | Block quote | - |
|
|
195
|
+
| `table` | Markdown table | - |
|
|
196
|
+
| `image` | Image | `url` (required), `alt` (optional) |
|
|
197
|
+
| `divider` | Horizontal rule | - |
|
|
198
|
+
|
|
199
|
+
Text content supports inline markdown: `**bold**`, `*italic*`, `` `code` ``, `[link](url)`
|
|
200
|
+
|
|
201
|
+
## Error Handling
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from surfacedocs import SurfaceDocs, SurfaceDocsError, AuthenticationError, ValidationError
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
result = client.save(content)
|
|
208
|
+
except AuthenticationError:
|
|
209
|
+
print("Invalid API key")
|
|
210
|
+
except ValidationError as e:
|
|
211
|
+
print(f"Invalid document: {e}")
|
|
212
|
+
except SurfaceDocsError as e:
|
|
213
|
+
print(f"API error: {e}")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Environment Variables
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# API key (alternative to passing in code)
|
|
220
|
+
export SURFACEDOCS_API_KEY=sd_live_...
|
|
221
|
+
|
|
222
|
+
# Override base URL (for testing)
|
|
223
|
+
export SURFACEDOCS_BASE_URL=https://ingress.dev.surfacedocs.dev
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The SDK auto-detects environment from API key prefix:
|
|
227
|
+
- `sd_live_*` → Production
|
|
228
|
+
- `sd_test_*` → Development
|
|
229
|
+
|
|
230
|
+
## Examples
|
|
231
|
+
|
|
232
|
+
### OpenAI
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from surfacedocs import SurfaceDocs, DOCUMENT_SCHEMA, SYSTEM_PROMPT
|
|
236
|
+
from openai import OpenAI
|
|
237
|
+
|
|
238
|
+
openai = OpenAI()
|
|
239
|
+
docs = SurfaceDocs()
|
|
240
|
+
|
|
241
|
+
response = openai.chat.completions.create(
|
|
242
|
+
model="gpt-4o",
|
|
243
|
+
messages=[
|
|
244
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
245
|
+
{"role": "user", "content": "Write documentation for user authentication"},
|
|
246
|
+
],
|
|
247
|
+
response_format={
|
|
248
|
+
"type": "json_schema",
|
|
249
|
+
"json_schema": {"name": "document", "schema": DOCUMENT_SCHEMA},
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
result = docs.save(response.choices[0].message.content)
|
|
254
|
+
print(f"Saved: {result.url}")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Anthropic
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from surfacedocs import SurfaceDocs, DOCUMENT_SCHEMA, SYSTEM_PROMPT
|
|
261
|
+
import anthropic
|
|
262
|
+
|
|
263
|
+
client = anthropic.Anthropic()
|
|
264
|
+
docs = SurfaceDocs()
|
|
265
|
+
|
|
266
|
+
response = client.messages.create(
|
|
267
|
+
model="claude-sonnet-4-20250514",
|
|
268
|
+
max_tokens=4096,
|
|
269
|
+
system=SYSTEM_PROMPT,
|
|
270
|
+
messages=[
|
|
271
|
+
{"role": "user", "content": "Write documentation for user authentication"},
|
|
272
|
+
],
|
|
273
|
+
tools=[{
|
|
274
|
+
"name": "create_document",
|
|
275
|
+
"description": "Create a structured document",
|
|
276
|
+
"input_schema": DOCUMENT_SCHEMA,
|
|
277
|
+
}],
|
|
278
|
+
tool_choice={"type": "tool", "name": "create_document"},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
tool_use = next(b for b in response.content if b.type == "tool_use")
|
|
282
|
+
result = docs.save(tool_use.input)
|
|
283
|
+
print(f"Saved: {result.url}")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Manual Document
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from surfacedocs import SurfaceDocs
|
|
290
|
+
|
|
291
|
+
docs = SurfaceDocs()
|
|
292
|
+
|
|
293
|
+
result = docs.save_raw(
|
|
294
|
+
title="Meeting Notes",
|
|
295
|
+
blocks=[
|
|
296
|
+
{"type": "heading", "content": "Action Items", "metadata": {"level": 1}},
|
|
297
|
+
{"type": "list", "content": "- Review PR #123\n- Update docs", "metadata": {"listType": "bullet"}},
|
|
298
|
+
{"type": "divider", "content": ""},
|
|
299
|
+
{"type": "paragraph", "content": "Next meeting: Monday 10am"},
|
|
300
|
+
],
|
|
301
|
+
metadata={"source": "meeting-bot"},
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Docstrings
|
|
313
|
+
|
|
314
|
+
Ensure all public APIs have docstrings:
|
|
315
|
+
|
|
316
|
+
- `SurfaceDocs.__init__`
|
|
317
|
+
- `SurfaceDocs.save`
|
|
318
|
+
- `SurfaceDocs.save_raw`
|
|
319
|
+
- `SaveResult` class
|
|
320
|
+
- All exception classes
|
|
321
|
+
- Module-level docstrings for `schema.py` and `prompt.py`
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Verification
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# README renders correctly
|
|
329
|
+
python -m readme_renderer README.md
|
|
330
|
+
|
|
331
|
+
# Or just check it's valid markdown
|
|
332
|
+
cat README.md
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Definition of Done
|
|
338
|
+
|
|
339
|
+
- [x] `README.md` with installation, quick start, API reference, examples
|
|
340
|
+
- [x] OpenAI example in README
|
|
341
|
+
- [x] Anthropic example in README
|
|
342
|
+
- [x] Block types documented
|
|
343
|
+
- [x] Error handling documented
|
|
344
|
+
- [x] All public APIs have docstrings
|
|
345
|
+
- [x] `LICENSE` file exists (MIT)
|