Skip to main content

origin_types/
requests.rs

1// SPDX-License-Identifier: Apache-2.0
2//! API request types for all HTTP endpoints.
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7// ===== Memory CRUD =====
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct StoreMemoryRequest {
11    pub content: String,
12    #[serde(default)]
13    pub memory_type: Option<String>,
14    #[serde(default, alias = "domain")]
15    pub space: Option<String>,
16    #[serde(default)]
17    pub source_agent: Option<String>,
18    #[serde(default)]
19    pub title: Option<String>,
20    #[serde(default)]
21    pub confidence: Option<f32>,
22    #[serde(default)]
23    pub supersedes: Option<String>,
24    /// Entity name for resolution (e.g. "Alice", "PostgreSQL")
25    #[serde(default)]
26    pub entity: Option<String>,
27    /// Direct entity ID (bypasses name resolution)
28    #[serde(default)]
29    pub entity_id: Option<String>,
30    #[serde(default)]
31    pub structured_fields: Option<serde_json::Value>,
32    #[serde(default)]
33    pub retrieval_cue: Option<String>,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37pub struct SearchMemoryRequest {
38    pub query: String,
39    #[serde(default = "default_limit")]
40    pub limit: usize,
41    #[serde(default)]
42    pub memory_type: Option<String>,
43    #[serde(default, alias = "domain")]
44    pub space: Option<String>,
45    #[serde(default)]
46    pub source_agent: Option<String>,
47    /// When `true` AND the daemon has a reranker wired (via
48    /// `ORIGIN_RERANKER_ENABLED=1`), results pass through a cross-encoder
49    /// reranker after the embedding+FTS hybrid step. When `true` but no
50    /// reranker is available, the daemon logs a warning and falls back to
51    /// the plain hybrid ordering. Default `false` to preserve current
52    /// behavior for callers that don't opt in.
53    #[serde(default)]
54    pub rerank: bool,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58pub struct ListMemoriesRequest {
59    #[serde(default)]
60    pub memory_type: Option<String>,
61    #[serde(default, alias = "domain")]
62    pub space: Option<String>,
63    #[serde(default = "default_list_limit")]
64    pub limit: usize,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub confirmed: Option<bool>,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70pub struct ConfirmRequest {
71    #[serde(default = "default_confirmed")]
72    pub confirmed: bool,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct ReclassifyMemoryRequest {
77    pub memory_type: String,
78}
79
80#[derive(Debug, Serialize, Deserialize)]
81pub struct ImportMemoriesRequest {
82    pub source: String,
83    pub content: String,
84    #[serde(default)]
85    pub label: Option<String>,
86}
87
88// ===== General search/context =====
89
90#[derive(Debug, Serialize, Deserialize)]
91pub struct SearchRequest {
92    pub query: String,
93    #[serde(default = "default_limit")]
94    pub limit: usize,
95    pub source_filter: Option<String>,
96    #[serde(default, alias = "domain")]
97    pub space: Option<String>,
98}
99
100#[doc(hidden)]
101#[derive(Debug, Serialize, Deserialize)]
102pub struct ContextRequest {
103    pub current_file: String,
104    pub cursor_prefix: String,
105    #[serde(default = "default_limit")]
106    pub limit: usize,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110pub struct ChatContextRequest {
111    #[serde(default)]
112    pub query: Option<String>,
113    #[serde(default)]
114    pub conversation_id: Option<String>,
115    #[serde(default = "default_max_chunks")]
116    pub max_chunks: usize,
117    #[serde(default)]
118    pub relevance_threshold: Option<f64>,
119    /// Deprecated: goal-typed memories were folded into identity by migration 45
120    /// (Phase 0). Daemon ignores this field — no goal-load path exists anymore.
121    /// Field stays for wire backward compat; will be removed in 0.4.
122    #[deprecated(
123        since = "0.3.2",
124        note = "Goal taxonomy folded into Identity by migration 45 (Phase 0). \
125                Daemon ignores this field. Will be removed in 0.4."
126    )]
127    #[serde(default = "default_true")]
128    pub include_goals: bool,
129    #[serde(default, alias = "domain")]
130    pub space: Option<String>,
131}
132
133// ===== Knowledge graph =====
134
135#[derive(Debug, Serialize, Deserialize)]
136pub struct CreateEntityRequest {
137    pub name: String,
138    pub entity_type: String,
139    #[serde(default, alias = "domain")]
140    pub space: Option<String>,
141    #[serde(default)]
142    pub source_agent: Option<String>,
143    #[serde(default)]
144    pub confidence: Option<f32>,
145}
146
147#[doc(hidden)]
148#[derive(Debug, Serialize, Deserialize)]
149pub struct CreateRelationRequest {
150    pub from_entity: String,
151    pub to_entity: String,
152    pub relation_type: String,
153    #[serde(default)]
154    pub source_agent: Option<String>,
155    #[serde(default)]
156    pub confidence: Option<f64>,
157    #[serde(default)]
158    pub explanation: Option<String>,
159    #[serde(default)]
160    pub source_memory_id: Option<String>,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164pub struct AddObservationRequest {
165    pub entity_id: String,
166    pub content: String,
167    #[serde(default)]
168    pub source_agent: Option<String>,
169    #[serde(default)]
170    pub confidence: Option<f32>,
171}
172
173#[doc(hidden)]
174#[derive(Debug, Serialize, Deserialize)]
175pub struct LinkEntityRequest {
176    pub source_id: String,
177    pub entity_id: String,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
181pub struct ListEntitiesRequest {
182    #[serde(default)]
183    pub entity_type: Option<String>,
184    #[serde(default, alias = "domain")]
185    pub space: Option<String>,
186}
187
188#[derive(Debug, Serialize, Deserialize)]
189pub struct SearchEntitiesRequest {
190    pub query: String,
191    #[serde(default = "default_entity_search_limit")]
192    pub limit: usize,
193}
194
195// ===== Profile & Agents =====
196
197#[derive(Debug, Serialize, Deserialize)]
198pub struct UpdateProfileRequest {
199    #[serde(default)]
200    pub name: Option<String>,
201    #[serde(default)]
202    pub display_name: Option<String>,
203    #[serde(default)]
204    pub email: Option<String>,
205    #[serde(default)]
206    pub bio: Option<String>,
207    #[serde(default)]
208    pub avatar_path: Option<String>,
209}
210
211#[derive(Debug, Serialize, Deserialize)]
212pub struct UpdateAgentRequest {
213    #[serde(default)]
214    pub agent_type: Option<String>,
215    #[serde(default)]
216    pub description: Option<String>,
217    #[serde(default)]
218    pub enabled: Option<bool>,
219    #[serde(default)]
220    pub trust_level: Option<String>,
221    /// Empty string clears the field; None leaves it unchanged.
222    #[serde(default)]
223    pub display_name: Option<String>,
224}
225
226// ===== Spaces =====
227
228#[derive(Debug, Serialize, Deserialize)]
229pub struct CreateSpaceRequest {
230    pub name: String,
231    pub description: Option<String>,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
235pub struct UpdateSpaceRequest {
236    pub new_name: Option<String>,
237    pub description: Option<String>,
238}
239
240// ===== Concepts =====
241
242#[doc(hidden)]
243#[derive(Debug, Serialize, Deserialize)]
244pub struct CreateConceptRequest {
245    pub title: String,
246    pub content: String,
247    #[serde(default)]
248    pub summary: Option<String>,
249    #[serde(default)]
250    pub entity_id: Option<String>,
251    #[serde(default, alias = "domain")]
252    pub space: Option<String>,
253    #[serde(default)]
254    pub source_memory_ids: Vec<String>,
255    #[serde(default)]
256    pub creation_kind: Option<String>,
257    /// Dedicated workspace axis (P3). When Some, persisted to `pages.workspace`.
258    /// Distinct from `space` (category column used for page_type filtering).
259    #[serde(default)]
260    pub workspace: Option<String>,
261}
262
263#[derive(Debug, Serialize, Deserialize)]
264pub struct SearchPagesRequest {
265    pub query: String,
266    #[serde(default)]
267    pub limit: Option<usize>,
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub page_type: Option<String>,
270}
271
272// ===== Ingest =====
273
274#[derive(Debug, Serialize, Deserialize)]
275pub struct IngestTextRequest {
276    pub source: String,
277    pub source_id: String,
278    pub title: String,
279    pub content: String,
280    pub url: Option<String>,
281    pub metadata: Option<HashMap<String, String>>,
282}
283
284#[doc(hidden)]
285#[derive(Debug, Serialize, Deserialize)]
286pub struct IngestWebpageRequest {
287    pub url: String,
288    pub title: String,
289    pub content: String,
290    pub metadata: Option<HashMap<String, String>>,
291}
292
293#[derive(Debug, Serialize, Deserialize)]
294pub struct IngestMemoryRequest {
295    pub source: String,
296    pub source_id: String,
297    pub title: String,
298    pub content: String,
299    pub url: Option<String>,
300    pub tags: Option<Vec<String>>,
301    pub metadata: Option<HashMap<String, String>>,
302}
303
304// ===== Sources =====
305
306#[doc(hidden)]
307#[derive(Debug, Serialize, Deserialize)]
308pub struct AddSourceRequest {
309    pub source_type: String,
310    /// Filesystem path as a string. Kept as `String` (not `PathBuf`) because
311    /// this is an HTTP wire format — the handler converts it to `PathBuf`.
312    pub path: String,
313}
314
315// ===== Config =====
316
317#[derive(Debug, Serialize, Deserialize)]
318pub struct UpdateConfigRequest {
319    #[serde(default)]
320    pub skip_apps: Option<Vec<String>>,
321    #[serde(default)]
322    pub skip_title_patterns: Option<Vec<String>>,
323    #[serde(default)]
324    pub private_browsing_detection: Option<bool>,
325    #[serde(default)]
326    pub setup_completed: Option<bool>,
327    #[serde(default)]
328    pub clipboard_enabled: Option<bool>,
329    #[serde(default)]
330    pub screen_capture_enabled: Option<bool>,
331    #[serde(default)]
332    pub remote_access_enabled: Option<bool>,
333    /// Anthropic model used for fast/routine tasks (e.g. classification, tagging).
334    #[serde(default)]
335    pub routine_model: Option<String>,
336    /// Anthropic model used for synthesis tasks (e.g. distillation, narrative).
337    #[serde(default)]
338    pub synthesis_model: Option<String>,
339    /// Base URL for an OpenAI-compatible external LLM endpoint.
340    #[serde(default)]
341    pub external_llm_endpoint: Option<String>,
342    /// Model identifier to use with the external LLM endpoint.
343    #[serde(default)]
344    pub external_llm_model: Option<String>,
345}
346
347// ===== Chunks / indexed files =====
348
349#[derive(Debug, Serialize, Deserialize)]
350pub struct DeleteByTimeRangeRequest {
351    pub start: i64,
352    pub end: i64,
353}
354
355#[derive(Debug, Serialize, Deserialize)]
356pub struct BulkDeleteItem {
357    pub source: String,
358    pub source_id: String,
359}
360
361#[derive(Debug, Serialize, Deserialize)]
362pub struct BulkDeleteRequest {
363    pub items: Vec<BulkDeleteItem>,
364}
365
366#[derive(Debug, Serialize, Deserialize)]
367pub struct UpdateChunkRequest {
368    pub content: String,
369}
370
371// ===== Entity / Observation CRUD =====
372
373#[derive(Debug, Serialize, Deserialize)]
374pub struct ConfirmEntityRequest {
375    #[serde(default = "default_confirmed")]
376    pub confirmed: bool,
377}
378
379#[derive(Debug, Serialize, Deserialize)]
380pub struct AddEntityObservationRequest {
381    pub content: String,
382    #[serde(default)]
383    pub source_agent: Option<String>,
384    #[serde(default)]
385    pub confidence: Option<f32>,
386}
387
388#[derive(Debug, Serialize, Deserialize)]
389pub struct UpdateObservationRequest {
390    pub content: String,
391}
392
393#[derive(Debug, Serialize, Deserialize)]
394pub struct ConfirmObservationRequest {
395    #[serde(default = "default_confirmed")]
396    pub confirmed: bool,
397}
398
399// ===== Spaces extended =====
400
401#[derive(Debug, Serialize, Deserialize)]
402pub struct ReorderSpaceRequest {
403    pub name: String,
404    pub new_order: i64,
405}
406
407#[derive(Debug, Serialize, Deserialize)]
408pub struct SetDocumentSpaceRequest {
409    pub space_name: String,
410}
411
412// ===== Tags =====
413
414#[derive(Debug, Serialize, Deserialize)]
415pub struct SetDocumentTagsRequest {
416    pub tags: Vec<String>,
417}
418
419// ===== Memory update =====
420
421#[derive(Debug, Serialize, Deserialize)]
422pub struct UpdateMemoryRequest {
423    #[serde(default)]
424    pub content: Option<String>,
425    #[serde(default, alias = "domain")]
426    pub space: Option<String>,
427    #[serde(default)]
428    pub confirmed: Option<bool>,
429    #[serde(default)]
430    pub memory_type: Option<String>,
431}
432
433#[derive(Debug, Serialize, Deserialize)]
434pub struct SetStabilityRequest {
435    pub stability: String,
436}
437
438#[derive(Debug, Serialize, Deserialize)]
439pub struct CorrectMemoryRequest {
440    pub correction_prompt: String,
441}
442
443// ===== Concepts update =====
444
445#[derive(Debug, Serialize, Deserialize)]
446pub struct UpdatePageRequest {
447    pub content: String,
448    /// Source memory IDs to associate with this page version.
449    /// Omitted or empty by HTTP callers that preserve existing sources;
450    /// always populated by `post_write::update_page`.
451    #[serde(default)]
452    pub source_memory_ids: Vec<String>,
453}
454
455/// Body for `PUT /api/pages/{id}` — agent-side refresh of a stale page.
456///
457/// Distinct from `UpdatePageRequest` (manual content edit via POST) because:
458///  - `source_memory_ids` is replaced, not preserved.
459///  - `summary` is optionally updated.
460///  - The handler clears `stale_reason` in the same transaction.
461///
462/// v1 deliberately excludes title / entity_id / space changes — slug rename
463/// has its own concurrent-read failure mode and lands as a separate route.
464#[derive(Debug, Serialize, Deserialize)]
465pub struct RefreshPageRequest {
466    pub content: String,
467    pub source_memory_ids: Vec<String>,
468    #[serde(default)]
469    pub summary: Option<String>,
470}
471
472// ===== Concept Export =====
473
474/// Request body for `POST /api/pages/export` (bulk export all pages to an Obsidian vault).
475#[derive(Debug, Deserialize, Serialize)]
476pub struct ExportPagesRequest {
477    pub vault_path: Option<String>,
478}
479
480#[derive(Debug, Deserialize, Serialize)]
481pub struct ExportPageRequest {
482    pub vault_path: String,
483}
484
485// ===== LLM test =====
486
487/// `POST /api/llm/test` — probe an OpenAI-compatible LLM endpoint with a 1-shot prompt.
488/// Used by the app settings UI to validate a custom endpoint before saving.
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct TestLlmRequest {
491    pub endpoint: String,
492    pub model: String,
493    /// Optional override prompt. Defaults to "Say 'hello' and nothing else." server-side.
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub prompt: Option<String>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct TestLlmResponse {
500    pub response: String,
501}
502
503// ===== Default value functions =====
504
505fn default_limit() -> usize {
506    10
507}
508
509fn default_list_limit() -> usize {
510    100
511}
512
513fn default_confirmed() -> bool {
514    true
515}
516
517fn default_max_chunks() -> usize {
518    5
519}
520
521fn default_true() -> bool {
522    true
523}
524
525fn default_entity_search_limit() -> usize {
526    20
527}
528
529#[cfg(test)]
530mod search_pages_page_type_test {
531    use super::*;
532
533    #[test]
534    fn search_pages_request_accepts_page_type() {
535        let json = r#"{"query":"foo","limit":10,"page_type":"recap"}"#;
536        let parsed: SearchPagesRequest = serde_json::from_str(json).unwrap();
537        assert_eq!(parsed.page_type.as_deref(), Some("recap"));
538    }
539
540    #[test]
541    fn search_pages_request_page_type_optional() {
542        let json = r#"{"query":"foo","limit":10}"#;
543        let parsed: SearchPagesRequest = serde_json::from_str(json).unwrap();
544        assert!(parsed.page_type.is_none());
545    }
546}
547
548#[cfg(test)]
549mod list_memories_confirmed_test {
550    use super::*;
551
552    #[test]
553    fn confirmed_false_serializes_to_json() {
554        let req = ListMemoriesRequest {
555            memory_type: None,
556            space: None,
557            limit: 20,
558            confirmed: Some(false),
559        };
560        let s = serde_json::to_string(&req).unwrap();
561        assert!(s.contains("\"confirmed\":false"), "got: {s}");
562    }
563
564    #[test]
565    fn confirmed_none_skips_serialization() {
566        let req = ListMemoriesRequest {
567            memory_type: None,
568            space: None,
569            limit: 20,
570            confirmed: None,
571        };
572        let s = serde_json::to_string(&req).unwrap();
573        assert!(!s.contains("confirmed"), "got: {s}");
574    }
575}