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.
@@ -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)