@octavus/docs 2.11.0 → 2.12.0
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.
- package/content/02-server-sdk/05-cli.md +9 -3
- package/content/03-client-sdk/08-file-uploads.md +57 -3
- package/content/04-protocol/01-overview.md +11 -6
- package/content/04-protocol/07-agent-config.md +21 -1
- package/content/04-protocol/12-references.md +189 -0
- package/content/05-api-reference/03-agents.md +31 -9
- package/dist/{chunk-EIUCL4CP.js → chunk-PQ5AGOPY.js} +3 -3
- package/dist/chunk-PQ5AGOPY.js.map +1 -0
- package/dist/{chunk-H6M6M3MY.js → chunk-RRXIH3DI.js} +35 -17
- package/dist/chunk-RRXIH3DI.js.map +1 -0
- package/dist/{chunk-NCTX3Y2J.js → chunk-SAB5XUB6.js} +35 -17
- package/dist/chunk-SAB5XUB6.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +17 -8
- package/dist/index.js +1 -1
- package/dist/search-index.json +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/dist/sections.json +17 -8
- package/package.json +1 -1
- package/dist/chunk-6TO62UOU.js +0 -1489
- package/dist/chunk-6TO62UOU.js.map +0 -1
- package/dist/chunk-EIUCL4CP.js.map +0 -1
- package/dist/chunk-H6M6M3MY.js.map +0 -1
- package/dist/chunk-NCTX3Y2J.js.map +0 -1
|
@@ -169,11 +169,17 @@ The CLI expects agent definitions in a specific directory structure:
|
|
|
169
169
|
my-agent/
|
|
170
170
|
├── settings.json # Required: Agent metadata
|
|
171
171
|
├── protocol.yaml # Required: Agent protocol
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
├── prompts/ # Optional: Prompt templates
|
|
173
|
+
│ ├── system.md
|
|
174
|
+
│ └── user-message.md
|
|
175
|
+
└── references/ # Optional: Reference documents
|
|
176
|
+
└── api-guidelines.md
|
|
175
177
|
```
|
|
176
178
|
|
|
179
|
+
### references/
|
|
180
|
+
|
|
181
|
+
Reference files are markdown documents with YAML frontmatter containing a `description`. The agent can fetch these on demand during execution. See [References](/docs/protocol/references) for details.
|
|
182
|
+
|
|
177
183
|
### settings.json
|
|
178
184
|
|
|
179
185
|
```json
|
|
@@ -77,6 +77,12 @@ function Chat({ sessionId }: { sessionId: string }) {
|
|
|
77
77
|
const { messages, status, send, uploadFiles } = useOctavusChat({
|
|
78
78
|
transport,
|
|
79
79
|
requestUploadUrls,
|
|
80
|
+
// Optional: configure upload timeout and retry behavior
|
|
81
|
+
uploadOptions: {
|
|
82
|
+
timeoutMs: 60_000, // Per-file timeout (default: 60s, set to 0 to disable)
|
|
83
|
+
maxRetries: 2, // Retry attempts on transient failures (default: 2)
|
|
84
|
+
retryDelayMs: 1_000, // Delay between retries (default: 1s)
|
|
85
|
+
},
|
|
80
86
|
});
|
|
81
87
|
|
|
82
88
|
// ...
|
|
@@ -176,6 +182,54 @@ async function handleSend(message: string, files?: File[]) {
|
|
|
176
182
|
|
|
177
183
|
The SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.
|
|
178
184
|
|
|
185
|
+
## Upload Reliability
|
|
186
|
+
|
|
187
|
+
Uploads include built-in timeout and retry logic for handling transient failures (network errors, server issues, mobile network switches).
|
|
188
|
+
|
|
189
|
+
**Default behavior:**
|
|
190
|
+
|
|
191
|
+
- **Timeout**: 60 seconds per file — prevents uploads from hanging on stalled connections
|
|
192
|
+
- **Retries**: 2 automatic retries on transient failures (network errors, 5xx, 429)
|
|
193
|
+
- **Retry delay**: 1 second between retries
|
|
194
|
+
- **Non-retryable errors** (4xx like 403, 404) fail immediately without retrying
|
|
195
|
+
|
|
196
|
+
Only the S3 upload is retried — the presigned URL stays valid for 15 minutes. On retry, the progress callback resets to 0%.
|
|
197
|
+
|
|
198
|
+
Configure via `uploadOptions`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const { send, uploadFiles } = useOctavusChat({
|
|
202
|
+
transport,
|
|
203
|
+
requestUploadUrls,
|
|
204
|
+
uploadOptions: {
|
|
205
|
+
timeoutMs: 120_000, // 2 minutes for large files
|
|
206
|
+
maxRetries: 3,
|
|
207
|
+
retryDelayMs: 2_000,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
To disable timeout or retries:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
uploadOptions: {
|
|
216
|
+
timeoutMs: 0, // No timeout
|
|
217
|
+
maxRetries: 0, // No retries
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Using `OctavusChat` Directly
|
|
222
|
+
|
|
223
|
+
When using the `OctavusChat` class directly (without the React hook), pass `uploadOptions` in the constructor:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const chat = new OctavusChat({
|
|
227
|
+
transport,
|
|
228
|
+
requestUploadUrls,
|
|
229
|
+
uploadOptions: { timeoutMs: 120_000, maxRetries: 3 },
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
179
233
|
## FileReference Type
|
|
180
234
|
|
|
181
235
|
File references contain metadata and URLs:
|
|
@@ -234,15 +288,15 @@ The `file` type is a built-in type representing uploaded files. Use `file[]` for
|
|
|
234
288
|
| Type | Media Types |
|
|
235
289
|
| --------- | -------------------------------------------------------------------- |
|
|
236
290
|
| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
|
|
291
|
+
| Video | `video/mp4`, `video/webm`, `video/quicktime`, `video/mpeg` |
|
|
237
292
|
| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |
|
|
238
293
|
|
|
239
294
|
## File Limits
|
|
240
295
|
|
|
241
296
|
| Limit | Value |
|
|
242
297
|
| --------------------- | ---------- |
|
|
243
|
-
| Max file size |
|
|
244
|
-
| Max total per request |
|
|
245
|
-
| Max files per request | 20 |
|
|
298
|
+
| Max file size | 100 MB |
|
|
299
|
+
| Max total per request | 200 MB |
|
|
246
300
|
| Upload URL expiry | 15 minutes |
|
|
247
301
|
| Download URL expiry | 24 hours |
|
|
248
302
|
|
|
@@ -105,16 +105,20 @@ Each agent is a folder with:
|
|
|
105
105
|
my-agent/
|
|
106
106
|
├── protocol.yaml # Main logic (required)
|
|
107
107
|
├── settings.json # Agent metadata (required)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
├── prompts/ # Prompt templates (supports subdirectories)
|
|
109
|
+
│ ├── system.md
|
|
110
|
+
│ ├── user-message.md
|
|
111
|
+
│ └── shared/
|
|
112
|
+
│ ├── company-info.md
|
|
113
|
+
│ └── formatting-rules.md
|
|
114
|
+
└── references/ # On-demand context documents (optional)
|
|
115
|
+
└── api-guidelines.md
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
Prompts can be organized in subdirectories. In the protocol, reference nested prompts by their path relative to `prompts/` (without `.md`): `shared/company-info`.
|
|
117
119
|
|
|
120
|
+
References are markdown files with YAML frontmatter that the agent can fetch on demand during execution. See [References](/docs/protocol/references).
|
|
121
|
+
|
|
118
122
|
### settings.json
|
|
119
123
|
|
|
120
124
|
```json
|
|
@@ -183,6 +187,7 @@ The referenced prompt content is inserted before variable interpolation, so vari
|
|
|
183
187
|
- [Triggers](/docs/protocol/triggers) — How agents are invoked
|
|
184
188
|
- [Tools](/docs/protocol/tools) — External capabilities
|
|
185
189
|
- [Skills](/docs/protocol/skills) — Code execution and knowledge packages
|
|
190
|
+
- [References](/docs/protocol/references) — On-demand context documents
|
|
186
191
|
- [Handlers](/docs/protocol/handlers) — Execution blocks
|
|
187
192
|
- [Agent Config](/docs/protocol/agent-config) — Model and settings
|
|
188
193
|
- [Workers](/docs/protocol/workers) — Worker agent format
|
|
@@ -15,6 +15,7 @@ agent:
|
|
|
15
15
|
system: system # References prompts/system.md
|
|
16
16
|
tools: [get-user-account] # Available tools
|
|
17
17
|
skills: [qr-code] # Available skills
|
|
18
|
+
references: [api-guidelines] # On-demand context documents
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Configuration Options
|
|
@@ -26,6 +27,7 @@ agent:
|
|
|
26
27
|
| `input` | No | Variables to pass to the system prompt |
|
|
27
28
|
| `tools` | No | List of tools the LLM can call |
|
|
28
29
|
| `skills` | No | List of Octavus skills the LLM can use |
|
|
30
|
+
| `references` | No | List of references the LLM can fetch on demand |
|
|
29
31
|
| `sandboxTimeout` | No | Skill sandbox timeout in ms (default: 5 min, max: 1 hour) |
|
|
30
32
|
| `imageModel` | No | Image generation model (enables agentic image generation) |
|
|
31
33
|
| `agentic` | No | Allow multiple tool call cycles |
|
|
@@ -212,6 +214,22 @@ Skills provide provider-agnostic code execution in isolated sandboxes. When enab
|
|
|
212
214
|
|
|
213
215
|
See [Skills](/docs/protocol/skills) for full documentation.
|
|
214
216
|
|
|
217
|
+
## References
|
|
218
|
+
|
|
219
|
+
Enable on-demand context loading via reference documents:
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
agent:
|
|
223
|
+
model: anthropic/claude-sonnet-4-5
|
|
224
|
+
system: system
|
|
225
|
+
references: [api-guidelines, error-codes]
|
|
226
|
+
agentic: true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
References are markdown files stored in the agent's `references/` directory. When enabled, the LLM can list available references and read their content using `octavus_reference_list` and `octavus_reference_read` tools.
|
|
230
|
+
|
|
231
|
+
See [References](/docs/protocol/references) for full documentation.
|
|
232
|
+
|
|
215
233
|
## Image Generation
|
|
216
234
|
|
|
217
235
|
Enable the LLM to generate images autonomously:
|
|
@@ -321,10 +339,11 @@ handlers:
|
|
|
321
339
|
maxSteps: 1 # Limit tool calls
|
|
322
340
|
system: escalation-summary # Different prompt
|
|
323
341
|
skills: [data-analysis] # Thread-specific skills
|
|
342
|
+
references: [escalation-policy] # Thread-specific references
|
|
324
343
|
imageModel: google/gemini-2.5-flash-image # Thread-specific image model
|
|
325
344
|
```
|
|
326
345
|
|
|
327
|
-
Each thread can have its own skills and image model. Skills
|
|
346
|
+
Each thread can have its own skills, references, and image model. Skills must be defined in the protocol's `skills:` section. References must exist in the agent's `references/` directory. Workers use this same pattern since they don't have a global `agent:` section.
|
|
328
347
|
|
|
329
348
|
## Full Example
|
|
330
349
|
|
|
@@ -372,6 +391,7 @@ agent:
|
|
|
372
391
|
- search-docs
|
|
373
392
|
- create-support-ticket
|
|
374
393
|
skills: [qr-code] # Octavus skills
|
|
394
|
+
references: [support-policies] # On-demand context
|
|
375
395
|
agentic: true
|
|
376
396
|
maxSteps: 10
|
|
377
397
|
thinking: medium
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: References
|
|
3
|
+
description: Using references for on-demand context loading in agents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# References
|
|
7
|
+
|
|
8
|
+
References are markdown documents that agents can fetch on demand. Instead of loading everything into the system prompt upfront, references let the agent decide what context it needs and load it when relevant.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
References are useful for:
|
|
13
|
+
|
|
14
|
+
- **Large context** — Documents too long to include in every system prompt
|
|
15
|
+
- **Selective loading** — Let the agent decide which context is relevant
|
|
16
|
+
- **Shared knowledge** — Reusable documents across threads
|
|
17
|
+
|
|
18
|
+
### How References Work
|
|
19
|
+
|
|
20
|
+
1. **Definition**: Reference files live in the `references/` directory alongside your agent
|
|
21
|
+
2. **Configuration**: List available references in `agent.references` or `start-thread.references`
|
|
22
|
+
3. **Discovery**: The agent sees reference names and descriptions in its system prompt
|
|
23
|
+
4. **Fetching**: The agent calls reference tools to read the full content when needed
|
|
24
|
+
|
|
25
|
+
## Creating References
|
|
26
|
+
|
|
27
|
+
Each reference is a markdown file with YAML frontmatter in the `references/` directory:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
my-agent/
|
|
31
|
+
├── settings.json
|
|
32
|
+
├── protocol.yaml
|
|
33
|
+
├── prompts/
|
|
34
|
+
│ └── system.md
|
|
35
|
+
└── references/
|
|
36
|
+
├── api-guidelines.md
|
|
37
|
+
└── error-codes.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Reference Format
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
---
|
|
44
|
+
description: >
|
|
45
|
+
API design guidelines including naming conventions,
|
|
46
|
+
error handling patterns, and pagination standards.
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# API Guidelines
|
|
50
|
+
|
|
51
|
+
## Naming Conventions
|
|
52
|
+
|
|
53
|
+
Use lowercase with dashes for URL paths...
|
|
54
|
+
|
|
55
|
+
## Error Handling
|
|
56
|
+
|
|
57
|
+
All errors return a standard error envelope...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The `description` field is required. It tells the agent what the reference contains so it can decide when to fetch it.
|
|
61
|
+
|
|
62
|
+
### Naming Convention
|
|
63
|
+
|
|
64
|
+
Reference filenames use `lowercase-with-dashes`:
|
|
65
|
+
|
|
66
|
+
- `api-guidelines.md`
|
|
67
|
+
- `error-codes.md`
|
|
68
|
+
- `coding-standards.md`
|
|
69
|
+
|
|
70
|
+
The filename (without `.md`) becomes the reference name used in the protocol.
|
|
71
|
+
|
|
72
|
+
## Enabling References
|
|
73
|
+
|
|
74
|
+
After creating reference files, specify which references are available in the protocol.
|
|
75
|
+
|
|
76
|
+
### Interactive Agents
|
|
77
|
+
|
|
78
|
+
List references in `agent.references`:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
agent:
|
|
82
|
+
model: anthropic/claude-sonnet-4-5
|
|
83
|
+
system: system
|
|
84
|
+
references: [api-guidelines, error-codes]
|
|
85
|
+
agentic: true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Workers and Named Threads
|
|
89
|
+
|
|
90
|
+
List references per-thread in `start-thread.references`:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
steps:
|
|
94
|
+
Start thread:
|
|
95
|
+
block: start-thread
|
|
96
|
+
thread: worker
|
|
97
|
+
model: anthropic/claude-sonnet-4-5
|
|
98
|
+
system: system
|
|
99
|
+
references: [api-guidelines]
|
|
100
|
+
maxSteps: 10
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Different threads can have different references.
|
|
104
|
+
|
|
105
|
+
## Reference Tools
|
|
106
|
+
|
|
107
|
+
When references are enabled, the agent has access to two tools:
|
|
108
|
+
|
|
109
|
+
| Tool | Purpose |
|
|
110
|
+
| ------------------------ | ----------------------------------------------- |
|
|
111
|
+
| `octavus_reference_list` | List all available references with descriptions |
|
|
112
|
+
| `octavus_reference_read` | Read the full content of a specific reference |
|
|
113
|
+
|
|
114
|
+
The agent also sees reference names and descriptions in its system prompt, so it knows what's available without calling `octavus_reference_list`.
|
|
115
|
+
|
|
116
|
+
## Example
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
agent:
|
|
120
|
+
model: anthropic/claude-sonnet-4-5
|
|
121
|
+
system: system
|
|
122
|
+
tools: [review-pull-request]
|
|
123
|
+
references: [coding-standards, api-guidelines]
|
|
124
|
+
agentic: true
|
|
125
|
+
|
|
126
|
+
handlers:
|
|
127
|
+
user-message:
|
|
128
|
+
Add message:
|
|
129
|
+
block: add-message
|
|
130
|
+
role: user
|
|
131
|
+
prompt: user-message
|
|
132
|
+
input: [USER_MESSAGE]
|
|
133
|
+
|
|
134
|
+
Respond:
|
|
135
|
+
block: next-message
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With `references/coding-standards.md`:
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
---
|
|
142
|
+
description: >
|
|
143
|
+
Team coding standards including naming conventions,
|
|
144
|
+
code organization, and review checklist.
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
# Coding Standards
|
|
148
|
+
|
|
149
|
+
## Naming Conventions
|
|
150
|
+
|
|
151
|
+
- Files: kebab-case
|
|
152
|
+
- Variables: camelCase
|
|
153
|
+
- Constants: UPPER_SNAKE_CASE
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
When a user asks the agent to review code, the agent will:
|
|
158
|
+
|
|
159
|
+
1. See "coding-standards" and "api-guidelines" in its system prompt
|
|
160
|
+
2. Decide which references are relevant to the review
|
|
161
|
+
3. Call `octavus_reference_read` to load the relevant reference
|
|
162
|
+
4. Use the loaded context to provide an informed review
|
|
163
|
+
|
|
164
|
+
## Validation
|
|
165
|
+
|
|
166
|
+
The CLI and platform validate references during sync and deployment:
|
|
167
|
+
|
|
168
|
+
- **Undefined references** — Referencing a name that doesn't have a matching file in `references/`
|
|
169
|
+
- **Unused references** — A reference file exists but isn't listed in any `agent.references` or `start-thread.references`
|
|
170
|
+
- **Invalid names** — Names that don't follow the `lowercase-with-dashes` convention
|
|
171
|
+
- **Missing description** — Reference files without the required `description` in frontmatter
|
|
172
|
+
|
|
173
|
+
## References vs Skills
|
|
174
|
+
|
|
175
|
+
| Aspect | References | Skills |
|
|
176
|
+
| ------------- | ----------------------------- | ------------------------------- |
|
|
177
|
+
| **Purpose** | On-demand context documents | Code execution and file output |
|
|
178
|
+
| **Content** | Markdown text | Documentation + scripts |
|
|
179
|
+
| **Execution** | Synchronous text retrieval | Sandboxed code execution (E2B) |
|
|
180
|
+
| **Scope** | Per-agent (stored with agent) | Per-organization (shared) |
|
|
181
|
+
| **Tools** | List and read (2 tools) | Read, list, run, code (6 tools) |
|
|
182
|
+
|
|
183
|
+
Use **references** when the agent needs access to text-based knowledge. Use **skills** when the agent needs to execute code or generate files.
|
|
184
|
+
|
|
185
|
+
## Next Steps
|
|
186
|
+
|
|
187
|
+
- [Agent Config](/docs/protocol/agent-config) — Configuring references in agent settings
|
|
188
|
+
- [Skills](/docs/protocol/skills) — Code execution and knowledge packages
|
|
189
|
+
- [Workers](/docs/protocol/workers) — Using references in worker agents
|
|
@@ -5,7 +5,7 @@ description: Agent management API endpoints.
|
|
|
5
5
|
|
|
6
6
|
# Agents API
|
|
7
7
|
|
|
8
|
-
Manage agent definitions including protocols and
|
|
8
|
+
Manage agent definitions including protocols, prompts, and references.
|
|
9
9
|
|
|
10
10
|
## Permissions
|
|
11
11
|
|
|
@@ -82,6 +82,13 @@ GET /api/agents/:id
|
|
|
82
82
|
"name": "user-message",
|
|
83
83
|
"content": "{{USER_MESSAGE}}"
|
|
84
84
|
}
|
|
85
|
+
],
|
|
86
|
+
"references": [
|
|
87
|
+
{
|
|
88
|
+
"name": "api-guidelines",
|
|
89
|
+
"description": "API design guidelines and conventions",
|
|
90
|
+
"content": "# API Guidelines\n\nUse lowercase with dashes..."
|
|
91
|
+
}
|
|
85
92
|
]
|
|
86
93
|
}
|
|
87
94
|
```
|
|
@@ -119,18 +126,26 @@ POST /api/agents
|
|
|
119
126
|
"name": "system",
|
|
120
127
|
"content": "You are a support agent..."
|
|
121
128
|
}
|
|
129
|
+
],
|
|
130
|
+
"references": [
|
|
131
|
+
{
|
|
132
|
+
"name": "api-guidelines",
|
|
133
|
+
"description": "API design guidelines and conventions",
|
|
134
|
+
"content": "# API Guidelines\n..."
|
|
135
|
+
}
|
|
122
136
|
]
|
|
123
137
|
}
|
|
124
138
|
```
|
|
125
139
|
|
|
126
|
-
| Field | Type | Required | Description
|
|
127
|
-
| ---------------------- | ------ | -------- |
|
|
128
|
-
| `settings.slug` | string | Yes | URL-safe identifier
|
|
129
|
-
| `settings.name` | string | Yes | Display name
|
|
130
|
-
| `settings.description` | string | No | Agent description
|
|
131
|
-
| `settings.format` | string | Yes | `interactive` or `worker`
|
|
132
|
-
| `protocol` | string | Yes | YAML protocol definition
|
|
133
|
-
| `prompts` | array | Yes | Prompt files
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
| ---------------------- | ------ | -------- | ------------------------------------------------ |
|
|
142
|
+
| `settings.slug` | string | Yes | URL-safe identifier |
|
|
143
|
+
| `settings.name` | string | Yes | Display name |
|
|
144
|
+
| `settings.description` | string | No | Agent description |
|
|
145
|
+
| `settings.format` | string | Yes | `interactive` or `worker` |
|
|
146
|
+
| `protocol` | string | Yes | YAML protocol definition |
|
|
147
|
+
| `prompts` | array | Yes | Prompt files |
|
|
148
|
+
| `references` | array | No | Reference documents (name, description, content) |
|
|
134
149
|
|
|
135
150
|
### Response
|
|
136
151
|
|
|
@@ -178,6 +193,13 @@ PATCH /api/agents/:id
|
|
|
178
193
|
"name": "system",
|
|
179
194
|
"content": "Updated system prompt..."
|
|
180
195
|
}
|
|
196
|
+
],
|
|
197
|
+
"references": [
|
|
198
|
+
{
|
|
199
|
+
"name": "api-guidelines",
|
|
200
|
+
"description": "Updated description",
|
|
201
|
+
"content": "Updated content..."
|
|
202
|
+
}
|
|
181
203
|
]
|
|
182
204
|
}
|
|
183
205
|
```
|
|
@@ -513,7 +513,7 @@ See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list
|
|
|
513
513
|
section: "client-sdk",
|
|
514
514
|
title: "File Uploads",
|
|
515
515
|
description: "Uploading images and files for vision models and document processing.",
|
|
516
|
-
content: "\n# File Uploads\n\nThe Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing.\n\n## Overview\n\nFile uploads follow a two-step flow:\n\n1. **Request upload URLs** from the platform via your backend\n2. **Upload files directly to S3** using presigned URLs\n3. **Send file references** with your message\n\nThis architecture keeps your API key secure on the server while enabling fast, direct uploads.\n\n## Setup\n\n### Backend: Upload URLs Endpoint\n\nCreate an endpoint that proxies upload URL requests to the Octavus platform:\n\n```typescript\n// app/api/upload-urls/route.ts (Next.js)\nimport { NextResponse } from 'next/server';\nimport { octavus } from '@/lib/octavus';\n\nexport async function POST(request: Request) {\n const { sessionId, files } = await request.json();\n\n // Get presigned URLs from Octavus\n const result = await octavus.files.getUploadUrls(sessionId, files);\n\n return NextResponse.json(result);\n}\n```\n\n### Client: Configure File Uploads\n\nPass `requestUploadUrls` to the chat hook:\n\n```tsx\nimport { useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n // Request upload URLs from your backend\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const response = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return response.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n // ...\n}\n```\n\n## Uploading Files\n\n### Method 1: Upload Before Sending\n\nFor the best UX (showing upload progress), upload files first, then send:\n\n```tsx\nimport { useState, useRef } from 'react';\nimport type { FileReference } from '@octavus/react';\n\nfunction ChatInput({ sessionId }: { sessionId: string }) {\n const [pendingFiles, setPendingFiles] = useState<FileReference[]>([]);\n const [uploading, setUploading] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n async function handleFileSelect(event: React.ChangeEvent<HTMLInputElement>) {\n const files = event.target.files;\n if (!files?.length) return;\n\n setUploading(true);\n try {\n // Upload files with progress tracking\n const fileRefs = await uploadFiles(files, (fileIndex, progress) => {\n console.log(`File ${fileIndex}: ${progress}%`);\n });\n setPendingFiles((prev) => [...prev, ...fileRefs]);\n } finally {\n setUploading(false);\n }\n }\n\n async function handleSend(message: string) {\n await send(\n 'user-message',\n {\n USER_MESSAGE: message,\n FILES: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n {\n userMessage: {\n content: message,\n files: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n },\n );\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* File preview */}\n {pendingFiles.map((file) => (\n <img key={file.id} src={file.url} alt={file.filename} className=\"h-16\" />\n ))}\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*,.pdf\"\n multiple\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n\n <button onClick={() => fileInputRef.current?.click()} disabled={uploading}>\n {uploading ? 'Uploading...' : 'Attach'}\n </button>\n </div>\n );\n}\n```\n\n### Method 2: Upload on Send (Automatic)\n\nFor simpler implementations, pass `File` objects directly:\n\n```tsx\nasync function handleSend(message: string, files?: File[]) {\n await send(\n 'user-message',\n { USER_MESSAGE: message, FILES: files },\n { userMessage: { content: message, files } },\n );\n}\n```\n\nThe SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.\n\n## FileReference Type\n\nFile references contain metadata and URLs:\n\n```typescript\ninterface FileReference {\n /** Unique file ID (platform-generated) */\n id: string;\n /** IANA media type (e.g., 'image/png', 'application/pdf') */\n mediaType: string;\n /** Presigned download URL (S3) */\n url: string;\n /** Original filename */\n filename?: string;\n /** File size in bytes */\n size?: number;\n}\n```\n\n## Protocol Integration\n\nTo accept files in your agent protocol, use the `file[]` type:\n\n```yaml\ntriggers:\n user-message:\n input:\n USER_MESSAGE:\n type: string\n description: The user's message\n FILES:\n type: file[]\n optional: true\n description: User-attached images for vision analysis\n\nhandlers:\n user-message:\n Add user message:\n block: add-message\n role: user\n prompt: user-message\n input:\n - USER_MESSAGE\n files:\n - FILES # Attach files to the message\n display: hidden\n\n Respond to user:\n block: next-message\n```\n\nThe `file` type is a built-in type representing uploaded files. Use `file[]` for arrays of files.\n\n## Supported File Types\n\n| Type | Media Types |\n| --------- | -------------------------------------------------------------------- |\n| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |\n| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |\n\n## File Limits\n\n| Limit | Value |\n| --------------------- | ---------- |\n| Max file size | 10 MB |\n| Max total per request | 50 MB |\n| Max files per request | 20 |\n| Upload URL expiry | 15 minutes |\n| Download URL expiry | 24 hours |\n\n## Rendering User Files\n\nUser-uploaded files appear as `UIFilePart` in user messages:\n\n```tsx\nfunction UserMessage({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'file') {\n if (part.mediaType.startsWith('image/')) {\n return (\n <img\n key={i}\n src={part.url}\n alt={part.filename || 'Uploaded image'}\n className=\"max-h-48 rounded-lg\"\n />\n );\n }\n return (\n <a key={i} href={part.url} className=\"text-blue-500\">\n \u{1F4C4} {part.filename}\n </a>\n );\n }\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Server SDK: Files API\n\nThe Server SDK provides direct access to the Files API:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\n// Get presigned upload URLs\nconst { files } = await client.files.getUploadUrls(sessionId, [\n { filename: 'photo.jpg', mediaType: 'image/jpeg', size: 102400 },\n { filename: 'doc.pdf', mediaType: 'application/pdf', size: 204800 },\n]);\n\n// files[0].id - Use in FileReference\n// files[0].uploadUrl - PUT to this URL to upload\n// files[0].downloadUrl - Use as FileReference.url\n```\n\n## Complete Example\n\nHere's a full chat input component with file upload:\n\n```tsx\n'use client';\n\nimport { useState, useRef, useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport, type FileReference } from '@octavus/react';\n\ninterface PendingFile {\n file: File;\n id: string;\n status: 'uploading' | 'done' | 'error';\n progress: number;\n fileRef?: FileReference;\n error?: string;\n}\n\nexport function Chat({ sessionId }: { sessionId: string }) {\n const [input, setInput] = useState('');\n const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileIdCounter = useRef(0);\n\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const res = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return res.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n const isUploading = pendingFiles.some((f) => f.status === 'uploading');\n const hasErrors = pendingFiles.some((f) => f.status === 'error');\n const allReady = pendingFiles.every((f) => f.status === 'done');\n\n async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {\n const files = Array.from(e.target.files ?? []);\n if (!files.length) return;\n e.target.value = '';\n\n const newPending: PendingFile[] = files.map((file) => ({\n file,\n id: `pending-${++fileIdCounter.current}`,\n status: 'uploading',\n progress: 0,\n }));\n\n setPendingFiles((prev) => [...prev, ...newPending]);\n\n for (const pending of newPending) {\n try {\n const [fileRef] = await uploadFiles([pending.file], (_, progress) => {\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, progress } : f)),\n );\n });\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, status: 'done', fileRef } : f)),\n );\n } catch (err) {\n setPendingFiles((prev) =>\n prev.map((f) =>\n f.id === pending.id ? { ...f, status: 'error', error: String(err) } : f,\n ),\n );\n }\n }\n }\n\n async function handleSubmit() {\n if ((!input.trim() && !pendingFiles.length) || !allReady) return;\n\n const fileRefs = pendingFiles.filter((f) => f.fileRef).map((f) => f.fileRef!);\n\n await send(\n 'user-message',\n {\n USER_MESSAGE: input,\n FILES: fileRefs.length > 0 ? fileRefs : undefined,\n },\n {\n userMessage: {\n content: input,\n files: fileRefs.length > 0 ? fileRefs : undefined,\n },\n },\n );\n\n setInput('');\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* Messages */}\n {messages.map((msg) => (\n <div key={msg.id}>{/* ... render message */}</div>\n ))}\n\n {/* Pending files */}\n {pendingFiles.length > 0 && (\n <div className=\"flex gap-2\">\n {pendingFiles.map((f) => (\n <div key={f.id} className=\"relative\">\n <img\n src={URL.createObjectURL(f.file)}\n alt={f.file.name}\n className=\"h-16 w-16 object-cover rounded\"\n />\n {f.status === 'uploading' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <span className=\"text-white text-xs\">{f.progress}%</span>\n </div>\n )}\n <button\n onClick={() => setPendingFiles((prev) => prev.filter((p) => p.id !== f.id))}\n className=\"absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5\"\n >\n \xD7\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Input */}\n <div className=\"flex gap-2\">\n <input type=\"file\" ref={fileInputRef} onChange={handleFileSelect} hidden />\n <button onClick={() => fileInputRef.current?.click()}>\u{1F4CE}</button>\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1\"\n />\n <button onClick={handleSubmit} disabled={isUploading || hasErrors}>\n {isUploading ? 'Uploading...' : 'Send'}\n </button>\n </div>\n </div>\n );\n}\n```\n",
|
|
516
|
+
content: "\n# File Uploads\n\nThe Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing.\n\n## Overview\n\nFile uploads follow a two-step flow:\n\n1. **Request upload URLs** from the platform via your backend\n2. **Upload files directly to S3** using presigned URLs\n3. **Send file references** with your message\n\nThis architecture keeps your API key secure on the server while enabling fast, direct uploads.\n\n## Setup\n\n### Backend: Upload URLs Endpoint\n\nCreate an endpoint that proxies upload URL requests to the Octavus platform:\n\n```typescript\n// app/api/upload-urls/route.ts (Next.js)\nimport { NextResponse } from 'next/server';\nimport { octavus } from '@/lib/octavus';\n\nexport async function POST(request: Request) {\n const { sessionId, files } = await request.json();\n\n // Get presigned URLs from Octavus\n const result = await octavus.files.getUploadUrls(sessionId, files);\n\n return NextResponse.json(result);\n}\n```\n\n### Client: Configure File Uploads\n\nPass `requestUploadUrls` to the chat hook:\n\n```tsx\nimport { useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n // Request upload URLs from your backend\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const response = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return response.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n // Optional: configure upload timeout and retry behavior\n uploadOptions: {\n timeoutMs: 60_000, // Per-file timeout (default: 60s, set to 0 to disable)\n maxRetries: 2, // Retry attempts on transient failures (default: 2)\n retryDelayMs: 1_000, // Delay between retries (default: 1s)\n },\n });\n\n // ...\n}\n```\n\n## Uploading Files\n\n### Method 1: Upload Before Sending\n\nFor the best UX (showing upload progress), upload files first, then send:\n\n```tsx\nimport { useState, useRef } from 'react';\nimport type { FileReference } from '@octavus/react';\n\nfunction ChatInput({ sessionId }: { sessionId: string }) {\n const [pendingFiles, setPendingFiles] = useState<FileReference[]>([]);\n const [uploading, setUploading] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n async function handleFileSelect(event: React.ChangeEvent<HTMLInputElement>) {\n const files = event.target.files;\n if (!files?.length) return;\n\n setUploading(true);\n try {\n // Upload files with progress tracking\n const fileRefs = await uploadFiles(files, (fileIndex, progress) => {\n console.log(`File ${fileIndex}: ${progress}%`);\n });\n setPendingFiles((prev) => [...prev, ...fileRefs]);\n } finally {\n setUploading(false);\n }\n }\n\n async function handleSend(message: string) {\n await send(\n 'user-message',\n {\n USER_MESSAGE: message,\n FILES: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n {\n userMessage: {\n content: message,\n files: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n },\n );\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* File preview */}\n {pendingFiles.map((file) => (\n <img key={file.id} src={file.url} alt={file.filename} className=\"h-16\" />\n ))}\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*,.pdf\"\n multiple\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n\n <button onClick={() => fileInputRef.current?.click()} disabled={uploading}>\n {uploading ? 'Uploading...' : 'Attach'}\n </button>\n </div>\n );\n}\n```\n\n### Method 2: Upload on Send (Automatic)\n\nFor simpler implementations, pass `File` objects directly:\n\n```tsx\nasync function handleSend(message: string, files?: File[]) {\n await send(\n 'user-message',\n { USER_MESSAGE: message, FILES: files },\n { userMessage: { content: message, files } },\n );\n}\n```\n\nThe SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.\n\n## Upload Reliability\n\nUploads include built-in timeout and retry logic for handling transient failures (network errors, server issues, mobile network switches).\n\n**Default behavior:**\n\n- **Timeout**: 60 seconds per file \u2014 prevents uploads from hanging on stalled connections\n- **Retries**: 2 automatic retries on transient failures (network errors, 5xx, 429)\n- **Retry delay**: 1 second between retries\n- **Non-retryable errors** (4xx like 403, 404) fail immediately without retrying\n\nOnly the S3 upload is retried \u2014 the presigned URL stays valid for 15 minutes. On retry, the progress callback resets to 0%.\n\nConfigure via `uploadOptions`:\n\n```typescript\nconst { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n uploadOptions: {\n timeoutMs: 120_000, // 2 minutes for large files\n maxRetries: 3,\n retryDelayMs: 2_000,\n },\n});\n```\n\nTo disable timeout or retries:\n\n```typescript\nuploadOptions: {\n timeoutMs: 0, // No timeout\n maxRetries: 0, // No retries\n}\n```\n\n### Using `OctavusChat` Directly\n\nWhen using the `OctavusChat` class directly (without the React hook), pass `uploadOptions` in the constructor:\n\n```typescript\nconst chat = new OctavusChat({\n transport,\n requestUploadUrls,\n uploadOptions: { timeoutMs: 120_000, maxRetries: 3 },\n});\n```\n\n## FileReference Type\n\nFile references contain metadata and URLs:\n\n```typescript\ninterface FileReference {\n /** Unique file ID (platform-generated) */\n id: string;\n /** IANA media type (e.g., 'image/png', 'application/pdf') */\n mediaType: string;\n /** Presigned download URL (S3) */\n url: string;\n /** Original filename */\n filename?: string;\n /** File size in bytes */\n size?: number;\n}\n```\n\n## Protocol Integration\n\nTo accept files in your agent protocol, use the `file[]` type:\n\n```yaml\ntriggers:\n user-message:\n input:\n USER_MESSAGE:\n type: string\n description: The user's message\n FILES:\n type: file[]\n optional: true\n description: User-attached images for vision analysis\n\nhandlers:\n user-message:\n Add user message:\n block: add-message\n role: user\n prompt: user-message\n input:\n - USER_MESSAGE\n files:\n - FILES # Attach files to the message\n display: hidden\n\n Respond to user:\n block: next-message\n```\n\nThe `file` type is a built-in type representing uploaded files. Use `file[]` for arrays of files.\n\n## Supported File Types\n\n| Type | Media Types |\n| --------- | -------------------------------------------------------------------- |\n| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |\n| Video | `video/mp4`, `video/webm`, `video/quicktime`, `video/mpeg` |\n| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |\n\n## File Limits\n\n| Limit | Value |\n| --------------------- | ---------- |\n| Max file size | 100 MB |\n| Max total per request | 200 MB |\n| Upload URL expiry | 15 minutes |\n| Download URL expiry | 24 hours |\n\n## Rendering User Files\n\nUser-uploaded files appear as `UIFilePart` in user messages:\n\n```tsx\nfunction UserMessage({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'file') {\n if (part.mediaType.startsWith('image/')) {\n return (\n <img\n key={i}\n src={part.url}\n alt={part.filename || 'Uploaded image'}\n className=\"max-h-48 rounded-lg\"\n />\n );\n }\n return (\n <a key={i} href={part.url} className=\"text-blue-500\">\n \u{1F4C4} {part.filename}\n </a>\n );\n }\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Server SDK: Files API\n\nThe Server SDK provides direct access to the Files API:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\n// Get presigned upload URLs\nconst { files } = await client.files.getUploadUrls(sessionId, [\n { filename: 'photo.jpg', mediaType: 'image/jpeg', size: 102400 },\n { filename: 'doc.pdf', mediaType: 'application/pdf', size: 204800 },\n]);\n\n// files[0].id - Use in FileReference\n// files[0].uploadUrl - PUT to this URL to upload\n// files[0].downloadUrl - Use as FileReference.url\n```\n\n## Complete Example\n\nHere's a full chat input component with file upload:\n\n```tsx\n'use client';\n\nimport { useState, useRef, useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport, type FileReference } from '@octavus/react';\n\ninterface PendingFile {\n file: File;\n id: string;\n status: 'uploading' | 'done' | 'error';\n progress: number;\n fileRef?: FileReference;\n error?: string;\n}\n\nexport function Chat({ sessionId }: { sessionId: string }) {\n const [input, setInput] = useState('');\n const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileIdCounter = useRef(0);\n\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const res = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return res.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n const isUploading = pendingFiles.some((f) => f.status === 'uploading');\n const hasErrors = pendingFiles.some((f) => f.status === 'error');\n const allReady = pendingFiles.every((f) => f.status === 'done');\n\n async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {\n const files = Array.from(e.target.files ?? []);\n if (!files.length) return;\n e.target.value = '';\n\n const newPending: PendingFile[] = files.map((file) => ({\n file,\n id: `pending-${++fileIdCounter.current}`,\n status: 'uploading',\n progress: 0,\n }));\n\n setPendingFiles((prev) => [...prev, ...newPending]);\n\n for (const pending of newPending) {\n try {\n const [fileRef] = await uploadFiles([pending.file], (_, progress) => {\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, progress } : f)),\n );\n });\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, status: 'done', fileRef } : f)),\n );\n } catch (err) {\n setPendingFiles((prev) =>\n prev.map((f) =>\n f.id === pending.id ? { ...f, status: 'error', error: String(err) } : f,\n ),\n );\n }\n }\n }\n\n async function handleSubmit() {\n if ((!input.trim() && !pendingFiles.length) || !allReady) return;\n\n const fileRefs = pendingFiles.filter((f) => f.fileRef).map((f) => f.fileRef!);\n\n await send(\n 'user-message',\n {\n USER_MESSAGE: input,\n FILES: fileRefs.length > 0 ? fileRefs : undefined,\n },\n {\n userMessage: {\n content: input,\n files: fileRefs.length > 0 ? fileRefs : undefined,\n },\n },\n );\n\n setInput('');\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* Messages */}\n {messages.map((msg) => (\n <div key={msg.id}>{/* ... render message */}</div>\n ))}\n\n {/* Pending files */}\n {pendingFiles.length > 0 && (\n <div className=\"flex gap-2\">\n {pendingFiles.map((f) => (\n <div key={f.id} className=\"relative\">\n <img\n src={URL.createObjectURL(f.file)}\n alt={f.file.name}\n className=\"h-16 w-16 object-cover rounded\"\n />\n {f.status === 'uploading' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <span className=\"text-white text-xs\">{f.progress}%</span>\n </div>\n )}\n <button\n onClick={() => setPendingFiles((prev) => prev.filter((p) => p.id !== f.id))}\n className=\"absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5\"\n >\n \xD7\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Input */}\n <div className=\"flex gap-2\">\n <input type=\"file\" ref={fileInputRef} onChange={handleFileSelect} hidden />\n <button onClick={() => fileInputRef.current?.click()}>\u{1F4CE}</button>\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1\"\n />\n <button onClick={handleSubmit} disabled={isUploading || hasErrors}>\n {isUploading ? 'Uploading...' : 'Send'}\n </button>\n </div>\n </div>\n );\n}\n```\n",
|
|
517
517
|
excerpt: "File Uploads The Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing. Overview File...",
|
|
518
518
|
order: 8
|
|
519
519
|
},
|
|
@@ -1236,7 +1236,7 @@ See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list
|
|
|
1236
1236
|
section: "client-sdk",
|
|
1237
1237
|
title: "File Uploads",
|
|
1238
1238
|
description: "Uploading images and files for vision models and document processing.",
|
|
1239
|
-
content: "\n# File Uploads\n\nThe Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing.\n\n## Overview\n\nFile uploads follow a two-step flow:\n\n1. **Request upload URLs** from the platform via your backend\n2. **Upload files directly to S3** using presigned URLs\n3. **Send file references** with your message\n\nThis architecture keeps your API key secure on the server while enabling fast, direct uploads.\n\n## Setup\n\n### Backend: Upload URLs Endpoint\n\nCreate an endpoint that proxies upload URL requests to the Octavus platform:\n\n```typescript\n// app/api/upload-urls/route.ts (Next.js)\nimport { NextResponse } from 'next/server';\nimport { octavus } from '@/lib/octavus';\n\nexport async function POST(request: Request) {\n const { sessionId, files } = await request.json();\n\n // Get presigned URLs from Octavus\n const result = await octavus.files.getUploadUrls(sessionId, files);\n\n return NextResponse.json(result);\n}\n```\n\n### Client: Configure File Uploads\n\nPass `requestUploadUrls` to the chat hook:\n\n```tsx\nimport { useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n // Request upload URLs from your backend\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const response = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return response.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n // ...\n}\n```\n\n## Uploading Files\n\n### Method 1: Upload Before Sending\n\nFor the best UX (showing upload progress), upload files first, then send:\n\n```tsx\nimport { useState, useRef } from 'react';\nimport type { FileReference } from '@octavus/react';\n\nfunction ChatInput({ sessionId }: { sessionId: string }) {\n const [pendingFiles, setPendingFiles] = useState<FileReference[]>([]);\n const [uploading, setUploading] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n async function handleFileSelect(event: React.ChangeEvent<HTMLInputElement>) {\n const files = event.target.files;\n if (!files?.length) return;\n\n setUploading(true);\n try {\n // Upload files with progress tracking\n const fileRefs = await uploadFiles(files, (fileIndex, progress) => {\n console.log(`File ${fileIndex}: ${progress}%`);\n });\n setPendingFiles((prev) => [...prev, ...fileRefs]);\n } finally {\n setUploading(false);\n }\n }\n\n async function handleSend(message: string) {\n await send(\n 'user-message',\n {\n USER_MESSAGE: message,\n FILES: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n {\n userMessage: {\n content: message,\n files: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n },\n );\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* File preview */}\n {pendingFiles.map((file) => (\n <img key={file.id} src={file.url} alt={file.filename} className=\"h-16\" />\n ))}\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*,.pdf\"\n multiple\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n\n <button onClick={() => fileInputRef.current?.click()} disabled={uploading}>\n {uploading ? 'Uploading...' : 'Attach'}\n </button>\n </div>\n );\n}\n```\n\n### Method 2: Upload on Send (Automatic)\n\nFor simpler implementations, pass `File` objects directly:\n\n```tsx\nasync function handleSend(message: string, files?: File[]) {\n await send(\n 'user-message',\n { USER_MESSAGE: message, FILES: files },\n { userMessage: { content: message, files } },\n );\n}\n```\n\nThe SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.\n\n## FileReference Type\n\nFile references contain metadata and URLs:\n\n```typescript\ninterface FileReference {\n /** Unique file ID (platform-generated) */\n id: string;\n /** IANA media type (e.g., 'image/png', 'application/pdf') */\n mediaType: string;\n /** Presigned download URL (S3) */\n url: string;\n /** Original filename */\n filename?: string;\n /** File size in bytes */\n size?: number;\n}\n```\n\n## Protocol Integration\n\nTo accept files in your agent protocol, use the `file[]` type:\n\n```yaml\ntriggers:\n user-message:\n input:\n USER_MESSAGE:\n type: string\n description: The user's message\n FILES:\n type: file[]\n optional: true\n description: User-attached images for vision analysis\n\nhandlers:\n user-message:\n Add user message:\n block: add-message\n role: user\n prompt: user-message\n input:\n - USER_MESSAGE\n files:\n - FILES # Attach files to the message\n display: hidden\n\n Respond to user:\n block: next-message\n```\n\nThe `file` type is a built-in type representing uploaded files. Use `file[]` for arrays of files.\n\n## Supported File Types\n\n| Type | Media Types |\n| --------- | -------------------------------------------------------------------- |\n| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |\n| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |\n\n## File Limits\n\n| Limit | Value |\n| --------------------- | ---------- |\n| Max file size | 10 MB |\n| Max total per request | 50 MB |\n| Max files per request | 20 |\n| Upload URL expiry | 15 minutes |\n| Download URL expiry | 24 hours |\n\n## Rendering User Files\n\nUser-uploaded files appear as `UIFilePart` in user messages:\n\n```tsx\nfunction UserMessage({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'file') {\n if (part.mediaType.startsWith('image/')) {\n return (\n <img\n key={i}\n src={part.url}\n alt={part.filename || 'Uploaded image'}\n className=\"max-h-48 rounded-lg\"\n />\n );\n }\n return (\n <a key={i} href={part.url} className=\"text-blue-500\">\n \u{1F4C4} {part.filename}\n </a>\n );\n }\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Server SDK: Files API\n\nThe Server SDK provides direct access to the Files API:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\n// Get presigned upload URLs\nconst { files } = await client.files.getUploadUrls(sessionId, [\n { filename: 'photo.jpg', mediaType: 'image/jpeg', size: 102400 },\n { filename: 'doc.pdf', mediaType: 'application/pdf', size: 204800 },\n]);\n\n// files[0].id - Use in FileReference\n// files[0].uploadUrl - PUT to this URL to upload\n// files[0].downloadUrl - Use as FileReference.url\n```\n\n## Complete Example\n\nHere's a full chat input component with file upload:\n\n```tsx\n'use client';\n\nimport { useState, useRef, useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport, type FileReference } from '@octavus/react';\n\ninterface PendingFile {\n file: File;\n id: string;\n status: 'uploading' | 'done' | 'error';\n progress: number;\n fileRef?: FileReference;\n error?: string;\n}\n\nexport function Chat({ sessionId }: { sessionId: string }) {\n const [input, setInput] = useState('');\n const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileIdCounter = useRef(0);\n\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const res = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return res.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n const isUploading = pendingFiles.some((f) => f.status === 'uploading');\n const hasErrors = pendingFiles.some((f) => f.status === 'error');\n const allReady = pendingFiles.every((f) => f.status === 'done');\n\n async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {\n const files = Array.from(e.target.files ?? []);\n if (!files.length) return;\n e.target.value = '';\n\n const newPending: PendingFile[] = files.map((file) => ({\n file,\n id: `pending-${++fileIdCounter.current}`,\n status: 'uploading',\n progress: 0,\n }));\n\n setPendingFiles((prev) => [...prev, ...newPending]);\n\n for (const pending of newPending) {\n try {\n const [fileRef] = await uploadFiles([pending.file], (_, progress) => {\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, progress } : f)),\n );\n });\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, status: 'done', fileRef } : f)),\n );\n } catch (err) {\n setPendingFiles((prev) =>\n prev.map((f) =>\n f.id === pending.id ? { ...f, status: 'error', error: String(err) } : f,\n ),\n );\n }\n }\n }\n\n async function handleSubmit() {\n if ((!input.trim() && !pendingFiles.length) || !allReady) return;\n\n const fileRefs = pendingFiles.filter((f) => f.fileRef).map((f) => f.fileRef!);\n\n await send(\n 'user-message',\n {\n USER_MESSAGE: input,\n FILES: fileRefs.length > 0 ? fileRefs : undefined,\n },\n {\n userMessage: {\n content: input,\n files: fileRefs.length > 0 ? fileRefs : undefined,\n },\n },\n );\n\n setInput('');\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* Messages */}\n {messages.map((msg) => (\n <div key={msg.id}>{/* ... render message */}</div>\n ))}\n\n {/* Pending files */}\n {pendingFiles.length > 0 && (\n <div className=\"flex gap-2\">\n {pendingFiles.map((f) => (\n <div key={f.id} className=\"relative\">\n <img\n src={URL.createObjectURL(f.file)}\n alt={f.file.name}\n className=\"h-16 w-16 object-cover rounded\"\n />\n {f.status === 'uploading' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <span className=\"text-white text-xs\">{f.progress}%</span>\n </div>\n )}\n <button\n onClick={() => setPendingFiles((prev) => prev.filter((p) => p.id !== f.id))}\n className=\"absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5\"\n >\n \xD7\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Input */}\n <div className=\"flex gap-2\">\n <input type=\"file\" ref={fileInputRef} onChange={handleFileSelect} hidden />\n <button onClick={() => fileInputRef.current?.click()}>\u{1F4CE}</button>\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1\"\n />\n <button onClick={handleSubmit} disabled={isUploading || hasErrors}>\n {isUploading ? 'Uploading...' : 'Send'}\n </button>\n </div>\n </div>\n );\n}\n```\n",
|
|
1239
|
+
content: "\n# File Uploads\n\nThe Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing.\n\n## Overview\n\nFile uploads follow a two-step flow:\n\n1. **Request upload URLs** from the platform via your backend\n2. **Upload files directly to S3** using presigned URLs\n3. **Send file references** with your message\n\nThis architecture keeps your API key secure on the server while enabling fast, direct uploads.\n\n## Setup\n\n### Backend: Upload URLs Endpoint\n\nCreate an endpoint that proxies upload URL requests to the Octavus platform:\n\n```typescript\n// app/api/upload-urls/route.ts (Next.js)\nimport { NextResponse } from 'next/server';\nimport { octavus } from '@/lib/octavus';\n\nexport async function POST(request: Request) {\n const { sessionId, files } = await request.json();\n\n // Get presigned URLs from Octavus\n const result = await octavus.files.getUploadUrls(sessionId, files);\n\n return NextResponse.json(result);\n}\n```\n\n### Client: Configure File Uploads\n\nPass `requestUploadUrls` to the chat hook:\n\n```tsx\nimport { useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n // Request upload URLs from your backend\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const response = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return response.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n // Optional: configure upload timeout and retry behavior\n uploadOptions: {\n timeoutMs: 60_000, // Per-file timeout (default: 60s, set to 0 to disable)\n maxRetries: 2, // Retry attempts on transient failures (default: 2)\n retryDelayMs: 1_000, // Delay between retries (default: 1s)\n },\n });\n\n // ...\n}\n```\n\n## Uploading Files\n\n### Method 1: Upload Before Sending\n\nFor the best UX (showing upload progress), upload files first, then send:\n\n```tsx\nimport { useState, useRef } from 'react';\nimport type { FileReference } from '@octavus/react';\n\nfunction ChatInput({ sessionId }: { sessionId: string }) {\n const [pendingFiles, setPendingFiles] = useState<FileReference[]>([]);\n const [uploading, setUploading] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n async function handleFileSelect(event: React.ChangeEvent<HTMLInputElement>) {\n const files = event.target.files;\n if (!files?.length) return;\n\n setUploading(true);\n try {\n // Upload files with progress tracking\n const fileRefs = await uploadFiles(files, (fileIndex, progress) => {\n console.log(`File ${fileIndex}: ${progress}%`);\n });\n setPendingFiles((prev) => [...prev, ...fileRefs]);\n } finally {\n setUploading(false);\n }\n }\n\n async function handleSend(message: string) {\n await send(\n 'user-message',\n {\n USER_MESSAGE: message,\n FILES: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n {\n userMessage: {\n content: message,\n files: pendingFiles.length > 0 ? pendingFiles : undefined,\n },\n },\n );\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* File preview */}\n {pendingFiles.map((file) => (\n <img key={file.id} src={file.url} alt={file.filename} className=\"h-16\" />\n ))}\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*,.pdf\"\n multiple\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n\n <button onClick={() => fileInputRef.current?.click()} disabled={uploading}>\n {uploading ? 'Uploading...' : 'Attach'}\n </button>\n </div>\n );\n}\n```\n\n### Method 2: Upload on Send (Automatic)\n\nFor simpler implementations, pass `File` objects directly:\n\n```tsx\nasync function handleSend(message: string, files?: File[]) {\n await send(\n 'user-message',\n { USER_MESSAGE: message, FILES: files },\n { userMessage: { content: message, files } },\n );\n}\n```\n\nThe SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.\n\n## Upload Reliability\n\nUploads include built-in timeout and retry logic for handling transient failures (network errors, server issues, mobile network switches).\n\n**Default behavior:**\n\n- **Timeout**: 60 seconds per file \u2014 prevents uploads from hanging on stalled connections\n- **Retries**: 2 automatic retries on transient failures (network errors, 5xx, 429)\n- **Retry delay**: 1 second between retries\n- **Non-retryable errors** (4xx like 403, 404) fail immediately without retrying\n\nOnly the S3 upload is retried \u2014 the presigned URL stays valid for 15 minutes. On retry, the progress callback resets to 0%.\n\nConfigure via `uploadOptions`:\n\n```typescript\nconst { send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n uploadOptions: {\n timeoutMs: 120_000, // 2 minutes for large files\n maxRetries: 3,\n retryDelayMs: 2_000,\n },\n});\n```\n\nTo disable timeout or retries:\n\n```typescript\nuploadOptions: {\n timeoutMs: 0, // No timeout\n maxRetries: 0, // No retries\n}\n```\n\n### Using `OctavusChat` Directly\n\nWhen using the `OctavusChat` class directly (without the React hook), pass `uploadOptions` in the constructor:\n\n```typescript\nconst chat = new OctavusChat({\n transport,\n requestUploadUrls,\n uploadOptions: { timeoutMs: 120_000, maxRetries: 3 },\n});\n```\n\n## FileReference Type\n\nFile references contain metadata and URLs:\n\n```typescript\ninterface FileReference {\n /** Unique file ID (platform-generated) */\n id: string;\n /** IANA media type (e.g., 'image/png', 'application/pdf') */\n mediaType: string;\n /** Presigned download URL (S3) */\n url: string;\n /** Original filename */\n filename?: string;\n /** File size in bytes */\n size?: number;\n}\n```\n\n## Protocol Integration\n\nTo accept files in your agent protocol, use the `file[]` type:\n\n```yaml\ntriggers:\n user-message:\n input:\n USER_MESSAGE:\n type: string\n description: The user's message\n FILES:\n type: file[]\n optional: true\n description: User-attached images for vision analysis\n\nhandlers:\n user-message:\n Add user message:\n block: add-message\n role: user\n prompt: user-message\n input:\n - USER_MESSAGE\n files:\n - FILES # Attach files to the message\n display: hidden\n\n Respond to user:\n block: next-message\n```\n\nThe `file` type is a built-in type representing uploaded files. Use `file[]` for arrays of files.\n\n## Supported File Types\n\n| Type | Media Types |\n| --------- | -------------------------------------------------------------------- |\n| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |\n| Video | `video/mp4`, `video/webm`, `video/quicktime`, `video/mpeg` |\n| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |\n\n## File Limits\n\n| Limit | Value |\n| --------------------- | ---------- |\n| Max file size | 100 MB |\n| Max total per request | 200 MB |\n| Upload URL expiry | 15 minutes |\n| Download URL expiry | 24 hours |\n\n## Rendering User Files\n\nUser-uploaded files appear as `UIFilePart` in user messages:\n\n```tsx\nfunction UserMessage({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'file') {\n if (part.mediaType.startsWith('image/')) {\n return (\n <img\n key={i}\n src={part.url}\n alt={part.filename || 'Uploaded image'}\n className=\"max-h-48 rounded-lg\"\n />\n );\n }\n return (\n <a key={i} href={part.url} className=\"text-blue-500\">\n \u{1F4C4} {part.filename}\n </a>\n );\n }\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Server SDK: Files API\n\nThe Server SDK provides direct access to the Files API:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\n// Get presigned upload URLs\nconst { files } = await client.files.getUploadUrls(sessionId, [\n { filename: 'photo.jpg', mediaType: 'image/jpeg', size: 102400 },\n { filename: 'doc.pdf', mediaType: 'application/pdf', size: 204800 },\n]);\n\n// files[0].id - Use in FileReference\n// files[0].uploadUrl - PUT to this URL to upload\n// files[0].downloadUrl - Use as FileReference.url\n```\n\n## Complete Example\n\nHere's a full chat input component with file upload:\n\n```tsx\n'use client';\n\nimport { useState, useRef, useMemo, useCallback } from 'react';\nimport { useOctavusChat, createHttpTransport, type FileReference } from '@octavus/react';\n\ninterface PendingFile {\n file: File;\n id: string;\n status: 'uploading' | 'done' | 'error';\n progress: number;\n fileRef?: FileReference;\n error?: string;\n}\n\nexport function Chat({ sessionId }: { sessionId: string }) {\n const [input, setInput] = useState('');\n const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileIdCounter = useRef(0);\n\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const requestUploadUrls = useCallback(\n async (files: { filename: string; mediaType: string; size: number }[]) => {\n const res = await fetch('/api/upload-urls', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, files }),\n });\n return res.json();\n },\n [sessionId],\n );\n\n const { messages, status, send, uploadFiles } = useOctavusChat({\n transport,\n requestUploadUrls,\n });\n\n const isUploading = pendingFiles.some((f) => f.status === 'uploading');\n const hasErrors = pendingFiles.some((f) => f.status === 'error');\n const allReady = pendingFiles.every((f) => f.status === 'done');\n\n async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {\n const files = Array.from(e.target.files ?? []);\n if (!files.length) return;\n e.target.value = '';\n\n const newPending: PendingFile[] = files.map((file) => ({\n file,\n id: `pending-${++fileIdCounter.current}`,\n status: 'uploading',\n progress: 0,\n }));\n\n setPendingFiles((prev) => [...prev, ...newPending]);\n\n for (const pending of newPending) {\n try {\n const [fileRef] = await uploadFiles([pending.file], (_, progress) => {\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, progress } : f)),\n );\n });\n setPendingFiles((prev) =>\n prev.map((f) => (f.id === pending.id ? { ...f, status: 'done', fileRef } : f)),\n );\n } catch (err) {\n setPendingFiles((prev) =>\n prev.map((f) =>\n f.id === pending.id ? { ...f, status: 'error', error: String(err) } : f,\n ),\n );\n }\n }\n }\n\n async function handleSubmit() {\n if ((!input.trim() && !pendingFiles.length) || !allReady) return;\n\n const fileRefs = pendingFiles.filter((f) => f.fileRef).map((f) => f.fileRef!);\n\n await send(\n 'user-message',\n {\n USER_MESSAGE: input,\n FILES: fileRefs.length > 0 ? fileRefs : undefined,\n },\n {\n userMessage: {\n content: input,\n files: fileRefs.length > 0 ? fileRefs : undefined,\n },\n },\n );\n\n setInput('');\n setPendingFiles([]);\n }\n\n return (\n <div>\n {/* Messages */}\n {messages.map((msg) => (\n <div key={msg.id}>{/* ... render message */}</div>\n ))}\n\n {/* Pending files */}\n {pendingFiles.length > 0 && (\n <div className=\"flex gap-2\">\n {pendingFiles.map((f) => (\n <div key={f.id} className=\"relative\">\n <img\n src={URL.createObjectURL(f.file)}\n alt={f.file.name}\n className=\"h-16 w-16 object-cover rounded\"\n />\n {f.status === 'uploading' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <span className=\"text-white text-xs\">{f.progress}%</span>\n </div>\n )}\n <button\n onClick={() => setPendingFiles((prev) => prev.filter((p) => p.id !== f.id))}\n className=\"absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5\"\n >\n \xD7\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Input */}\n <div className=\"flex gap-2\">\n <input type=\"file\" ref={fileInputRef} onChange={handleFileSelect} hidden />\n <button onClick={() => fileInputRef.current?.click()}>\u{1F4CE}</button>\n <input\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1\"\n />\n <button onClick={handleSubmit} disabled={isUploading || hasErrors}>\n {isUploading ? 'Uploading...' : 'Send'}\n </button>\n </div>\n </div>\n );\n}\n```\n",
|
|
1240
1240
|
excerpt: "File Uploads The Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing. Overview File...",
|
|
1241
1241
|
order: 8
|
|
1242
1242
|
},
|
|
@@ -1486,4 +1486,4 @@ export {
|
|
|
1486
1486
|
getDocSlugs,
|
|
1487
1487
|
getSectionBySlug
|
|
1488
1488
|
};
|
|
1489
|
-
//# sourceMappingURL=chunk-
|
|
1489
|
+
//# sourceMappingURL=chunk-PQ5AGOPY.js.map
|