Merge pull request #1372 from trheyi/main

Refactor chat storage design by replacing history with message
This commit is contained in:
Max 2025-12-09 10:21:12 +08:00 committed by GitHub
commit f040361f44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1118 additions and 436 deletions

View file

@ -13,6 +13,7 @@ const (
TypeThinking = "thinking" // Reasoning/thinking process
TypeLoading = "loading" // Loading/processing indicator
TypeToolCall = "tool_call" // LLM tool/function call
TypeRetrieval = "retrieval" // KB/Web search results (for feedback & analytics)
TypeError = "error" // Error message
TypeImage = "image" // Image content
TypeAudio = "audio" // Audio content
@ -281,7 +282,137 @@ msg := output.NewToolCallMessage(
---
### 6. Error (`error`)
### 6. Retrieval (`retrieval`)
**Purpose:** Knowledge base and web search results (for feedback, analytics, and source attribution)
**Props Structure:**
```go
type RetrievalProps struct {
Query string `json:"query"` // Search query
Sources []RetrievalSource `json:"sources"` // Retrieved sources
TotalResults int `json:"total_results,omitempty"` // Total matching results
QueryTimeMs int64 `json:"query_time_ms,omitempty"` // Query execution time
Provider string `json:"provider,omitempty"` // Search provider (e.g., "tavily", "bing")
}
type RetrievalSource struct {
ID string `json:"id"` // Unique source ID within this retrieval
Type string `json:"type"` // Source type: "kb", "web", "file", "api", "mcp"
Title string `json:"title,omitempty"` // Source title
Content string `json:"content"` // Retrieved content/snippet
Score float64 `json:"score,omitempty"` // Relevance score
URL string `json:"url,omitempty"` // URL for web sources
CollectionID string `json:"collection_id,omitempty"` // KB collection ID
DocumentID string `json:"document_id,omitempty"` // KB document ID
ChunkID string `json:"chunk_id,omitempty"` // KB chunk ID
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata
}
```
**Example (Knowledge Base):**
```json
{
"type": "retrieval",
"props": {
"query": "How to configure Yao models?",
"sources": [
{
"id": "src_001",
"type": "kb",
"collection_id": "col_docs",
"document_id": "doc_123",
"chunk_id": "chunk_456",
"title": "Model Configuration Guide",
"content": "To configure a model in Yao, create a .mod.yao file...",
"score": 0.92,
"metadata": {
"file_path": "/docs/model.md",
"page": 3
}
}
],
"total_results": 15,
"query_time_ms": 120
}
}
```
**Example (Web Search):**
```json
{
"type": "retrieval",
"props": {
"query": "latest AI news 2024",
"sources": [
{
"id": "src_001",
"type": "web",
"url": "https://example.com/ai-news",
"title": "AI Breakthroughs in 2024",
"content": "Summary of the article...",
"score": 0.95,
"metadata": {
"domain": "example.com",
"published_at": "2024-01-10"
}
}
],
"provider": "tavily",
"total_results": 10,
"query_time_ms": 850
}
}
```
**Helper:**
```go
msg := output.NewRetrievalMessage(
"How to configure Yao models?",
[]output.RetrievalSource{
{
ID: "src_001",
Type: "kb",
CollectionID: "col_docs",
DocumentID: "doc_123",
ChunkID: "chunk_456",
Title: "Model Configuration Guide",
Content: "To configure a model in Yao...",
Score: 0.92,
},
},
)
```
**Source Types:**
| Type | Description | Key Fields |
| ------ | ----------------------- | ------------------------------------------ |
| `kb` | Knowledge base document | `collection_id`, `document_id`, `chunk_id` |
| `web` | Web search result | `url` |
| `file` | Uploaded file | `file_id`, `file_path` |
| `api` | External API result | `api_name`, `endpoint` |
| `mcp` | MCP tool result | `server`, `tool` |
**Use Cases:**
- **Source Attribution**: Display citations in the chat UI
- **User Feedback**: Allow users to rate individual sources (👍/👎)
- **Analytics**: Track which documents/sources are most useful
- **RAG Optimization**: Improve retrieval based on feedback data
**Adapter Behavior:**
- **CUI**: Renders as expandable source cards with feedback buttons
- **OpenAI**: Converts to markdown citations or footnotes
---
### 7. Error (`error`)
**Purpose:** Error message
@ -316,7 +447,7 @@ msg := output.NewErrorMessage("Connection timeout", "TIMEOUT")
---
### 7. Action (`action`)
### 8. Action (`action`)
**Purpose:** System-level action/command (not displayed to user, only processed by client)
@ -387,7 +518,7 @@ output.Send(ctx, output.NewTextMessage("I've opened the user details panel for y
---
### 8. Event (`event`)
### 9. Event (`event`)
**Purpose:** Lifecycle event messages (stream_start, stream_end, connecting, etc.)
@ -479,7 +610,7 @@ output.Send(ctx, output.NewEventMessage("stream_end", "Stream completed", endDat
---
### 9. Image (`image`)
### 10. Image (`image`)
**Purpose:** Image content
@ -522,7 +653,7 @@ msg := output.NewImageMessage("https://example.com/avatar.jpg", "User avatar")
---
### 10. Audio (`audio`)
### 11. Audio (`audio`)
**Purpose:** Audio content
@ -567,7 +698,7 @@ msg := output.NewAudioMessage("https://example.com/audio.mp3", "mp3")
---
### 11. Video (`video`)
### 12. Video (`video`)
**Purpose:** Video content
@ -644,6 +775,7 @@ OpenAI adapter converts built-in types to OpenAI format:
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
| `tool_call` | `delta.tool_calls` | `props.{id, name, arguments}` | |
| `retrieval` | `delta.content` | `props.sources` | Markdown citations/footnotes with source links |
| `error` | `error` | `props.{message, code}` | |
| `image` | `delta.content` | `props.{url, alt}` | Markdown: `![alt](url)` - displays inline |
| `audio` | `delta.content` | `props.url` | Markdown link (can't display inline) |

View file

@ -93,7 +93,6 @@ Stores chat metadata and session information.
| `assistant_id` | string(200) | No | Yes | Associated assistant ID |
| `mode` | string(50) | No | - | Chat mode (default: "chat") |
| `status` | enum | No | Yes | Status: `active`, `archived` |
| `preset` | boolean | No | - | Whether this is a preset chat |
| `public` | boolean | No | - | Whether shared across all teams |
| `share` | enum | No | Yes | Sharing scope: `private`, `team` |
| `sort` | integer | No | - | Sort order for display |
@ -152,7 +151,7 @@ Stores user-visible messages (both user input and assistant responses).
| `block_id` | string(64) | Yes | Yes | Block grouping ID |
| `thread_id` | string(64) | Yes | Yes | Thread grouping ID |
| `assistant_id` | string(200) | Yes | Yes | Assistant ID (join to get name/avatar) |
| `sequence` | integer | No | Yes | Message order within chat |
| `sequence` | integer | No | - | Message order within chat (in composite) |
| `metadata` | json | Yes | - | Additional metadata |
| `created_at` | timestamp | No | Yes | Creation timestamp |
| `updated_at` | timestamp | No | - | Last update timestamp |
@ -163,28 +162,34 @@ Stores user-visible messages (both user input and assistant responses).
| ------------------- | --------------------- | ----- |
| `idx_msg_chat_seq` | `chat_id`, `sequence` | index |
| `idx_msg_request` | `request_id` | index |
| `idx_msg_role` | `role` | index |
| `idx_msg_block` | `block_id` | index |
| `idx_msg_thread` | `thread_id` | index |
| `idx_msg_assistant` | `assistant_id` | index |
**Message Types (Built-in):**
**Message Types:**
All built-in types defined in `agent/output/BUILTIN_TYPES.md` are stored. See that document for complete Props structures.
All message types are stored, including built-in types and custom types. See `agent/output/BUILTIN_TYPES.md` for built-in Props structures.
| Type | Description | Props Example | Stored? |
| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | ----------- |
| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | ------- |
| `user_input` | User input (frontend display) | `{"content": "Hello", "role": "user", "name": "John"}` | ✅ Yes |
| `text` | Text/Markdown content | `{"content": "Hello **world**!"}` | ✅ Yes |
| `thinking` | Reasoning process (o1, DeepSeek) | `{"content": "Let me analyze..."}` | ✅ Yes |
| `loading` | Loading/processing indicator | `{"message": "Searching knowledge base..."}` | ✅ Yes |
| `tool_call` | LLM tool/function call | `{"id": "call_abc123", "name": "get_weather", "arguments": "{\"location\":\"SF\"}"}` | ✅ Yes |
| `retrieval` | KB/Web search results | `{"query": "...", "sources": [...], "total_results": 10}` | ✅ Yes |
| `error` | Error message | `{"message": "Connection timeout", "code": "TIMEOUT", "details": "..."}` | ✅ Yes |
| `image` | Image content | `{"url": "...", "alt": "...", "width": 200, "height": 200, "detail": "auto"}` | ✅ Yes |
| `audio` | Audio content | `{"url": "...", "format": "mp3", "duration": 120.5, "transcript": "...", "controls": true}` | ✅ Yes |
| `video` | Video content | `{"url": "...", "format": "mp4", "thumbnail": "...", "width": 640, "height": 360}` | ✅ Yes |
| `action` | System action (CUI only) | `{"name": "open_panel", "payload": {"panel_id": "user_profile"}}` | ✅ Yes |
| `event` | Lifecycle event (CUI only) | `{"event": "stream_start", "message": "...", "data": {...}}` | ⚠️ Optional |
| `event` | Lifecycle event (CUI only) | `{"event": "stream_start", "message": "...", "data": {...}}` | ❌ No |
| `*` (custom) | Any custom type | `{"chartType": "bar", "data": [...], "options": {...}}` | ✅ Yes |
**Note on `event` type:** Lifecycle events (`stream_start`, `stream_end`, etc.) are typically transient and may not need persistent storage. Consider storing only significant events or skipping entirely based on use case.
**Note on `event` type:** Lifecycle events (`stream_start`, `stream_end`, etc.) are transient control signals and are NOT stored. They are only used for real-time streaming coordination.
**Note on custom types:** Any type not in the built-in list is stored as-is with its original `type` and `props` structure.
**Tool Call Storage:**
@ -272,6 +277,157 @@ User input with multimodal content (text + images + files) is stored as `user_in
}
```
### Knowledge Base & Web Search Results
Retrieval results from knowledge bases and web searches need to be stored for:
1. **User Feedback** - Users can rate (👍/👎) individual sources
2. **Quality Analytics** - Track which documents/sources are most useful
3. **Source Attribution** - Display citations in the UI
4. **RAG Optimization** - Improve retrieval based on feedback
**Storage Approach:** Store retrieval results as a special message type `retrieval` with structured props.
**Retrieval Message Structure:**
```json
{
"message_id": "msg_retrieval_001",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "retrieval",
"props": {
"query": "How to configure Yao models?",
"sources": [
{
"id": "src_001",
"type": "kb",
"collection_id": "col_docs",
"document_id": "doc_123",
"chunk_id": "chunk_456",
"title": "Model Configuration Guide",
"content": "To configure a model in Yao, create a .mod.yao file...",
"score": 0.92,
"metadata": {
"file_path": "/docs/model.md",
"page": 3
}
},
{
"id": "src_002",
"type": "kb",
"collection_id": "col_docs",
"document_id": "doc_124",
"chunk_id": "chunk_789",
"title": "Advanced Model Options",
"content": "Models support various options including soft_deletes...",
"score": 0.87,
"metadata": {
"file_path": "/docs/advanced.md",
"page": 12
}
},
{
"id": "src_003",
"type": "web",
"url": "https://yaoapps.com/docs/models",
"title": "Yao Models Documentation",
"content": "Official documentation for Yao model system...",
"score": 0.85,
"metadata": {
"domain": "yaoapps.com",
"fetched_at": "2024-01-15T10:30:00Z"
}
}
],
"total_results": 15,
"query_time_ms": 120
},
"block_id": "B1",
"assistant_id": "docs_assistant",
"sequence": 2
}
```
**Source Types:**
| Type | Description | Key Fields |
| ------ | ----------------------- | ------------------------------------------ |
| `kb` | Knowledge base document | `collection_id`, `document_id`, `chunk_id` |
| `web` | Web search result | `url`, `domain` |
| `file` | Uploaded file | `file_id`, `file_path` |
| `api` | External API result | `api_name`, `endpoint` |
| `mcp` | MCP tool result | `server`, `tool` |
**Source Feedback:**
User feedback on retrieval sources is handled by the Knowledge Base module. See [KB Feedback](../../kb/README.md) for details.
**Example: KB Search in Create Hook:**
```typescript
// In Create hook, search knowledge base and store results
const results = await ctx.kb.search("col_docs", query, { limit: 5 });
// Send retrieval message (stored automatically)
ctx.Send({
type: "retrieval",
props: {
query: query,
sources: results.documents.map((doc, idx) => ({
id: `src_${idx}`,
type: "kb",
collection_id: "col_docs",
document_id: doc.document.metadata.document_id,
chunk_id: doc.document.id,
title: doc.document.metadata.title || "Untitled",
content: doc.document.content,
score: doc.score,
metadata: doc.document.metadata,
})),
total_results: results.total,
query_time_ms: results.query_time_ms,
},
});
// Also send loading message for user feedback
ctx.Send({
type: "loading",
props: { message: `Found ${results.total} relevant documents...` },
});
```
**Example: Web Search Results:**
```json
{
"type": "retrieval",
"props": {
"query": "latest AI news 2024",
"sources": [
{
"id": "src_001",
"type": "web",
"url": "https://example.com/ai-news",
"title": "AI Breakthroughs in 2024",
"content": "Summary of the article...",
"score": 0.95,
"metadata": {
"domain": "example.com",
"published_at": "2024-01-10",
"fetched_at": "2024-01-15T10:30:00Z",
"snippet": "The year 2024 has seen remarkable..."
}
}
],
"provider": "tavily",
"total_results": 10,
"query_time_ms": 850
}
}
```
### 3. Resume Table
Stores execution state for resume/retry functionality. **Only written when request is interrupted or failed.**
@ -279,7 +435,7 @@ Stores execution state for resume/retry functionality. **Only written when reque
**Table Name:** `agent_resume`
| Column | Type | Nullable | Index | Description |
| ----------------- | ----------- | -------- | ------ | -------------------------------- |
| ----------------- | ----------- | -------- | ------ | ---------------------------------------- |
| `id` | ID | No | PK | Auto-increment primary key |
| `resume_id` | string(64) | No | Unique | Unique resume record identifier |
| `chat_id` | string(64) | No | Yes | Parent chat ID |
@ -294,7 +450,7 @@ Stores execution state for resume/retry functionality. **Only written when reque
| `output` | json | Yes | - | Step output data (partial) |
| `space_snapshot` | json | Yes | - | Space data snapshot for recovery |
| `error` | text | Yes | - | Error message if failed |
| `sequence` | integer | No | Yes | Step order within request |
| `sequence` | integer | No | - | Step order within request (in composite) |
| `metadata` | json | Yes | - | Additional metadata |
| `created_at` | timestamp | No | Yes | Creation timestamp |
| `updated_at` | timestamp | No | - | Last update timestamp |
@ -344,6 +500,7 @@ If interrupted during delegate, the `space_snapshot` allows restoring `ctx.Space
| ---------------------- | ------------------------ | ----- |
| `idx_resume_chat` | `chat_id` | index |
| `idx_resume_request` | `request_id`, `sequence` | index |
| `idx_resume_type` | `type` | index |
| `idx_resume_status` | `status` | index |
| `idx_resume_stack` | `stack_id` | index |
| `idx_resume_parent` | `stack_parent_id` | index |
@ -351,20 +508,16 @@ If interrupted during delegate, the `space_snapshot` allows restoring `ctx.Space
## Write Strategy
### Two-Write Strategy
### Single-Write Strategy
All data is buffered in memory during execution and written to database only **twice**:
1. **Write 1 (Entry)**: When `Stream()` starts - save user input message
2. **Write 2 (Exit)**: When `Stream()` exits - batch save messages (and steps only on error/interrupt)
All data is buffered in memory during execution and written to database **only once** when `Stream()` exits:
**Note**: Request tracking (status, tokens, duration) is handled by [OpenAPI Request Middleware](../../openapi/request/REQUEST_DESIGN.md).
```
Stream() Entry
├── 【Write 1】Save user input
│ - User message (role=user)
├── Buffer user input message (role=user)
├── Execution (all in memory)
│ - ctx.Send() → messageBuffer
@ -372,10 +525,10 @@ Stream() Entry
│ - ctx.Replace() → update messageBuffer
│ - Each step → stepBuffer
└── 【Write 2】Save final state (via defer)
└── 【Single Write】Save final state (via defer)
├── Always:
│ - Batch write all assistant messages
│ - Batch write all messages (user input + assistant responses)
│ - Update token usage in openapi_request (via request_id)
└── Only on error/interrupt:
@ -385,58 +538,58 @@ Stream() Entry
### Write Points
| Event | Message Table | Step Table | Token Usage |
| ---------------- | -------------------- | ----------------------------------- | ----------- |
| Stream entry | Write 1 (user input) | - | - |
| ---------------- | -------------------------------------- | ----------------------------------- | ----------- |
| Stream entry | Buffer user input | - | - |
| During execution | Buffer in memory | Buffer in memory | - |
| **Completed** | **Batch write all** | **❌ Skip (no need to resume)** | ✅ Update |
| On interrupt | Batch write buffered | ✅ Batch write (status=interrupted) | ✅ Update |
| On error | Batch write buffered | ✅ Batch write (status=failed) | ✅ Update |
| **Completed** | **Batch write all (user + assistant)** | **❌ Skip (no need to resume)** | ✅ Update |
| On interrupt | Batch write all buffered | ✅ Batch write (status=interrupted) | ✅ Update |
| On error | Batch write all buffered | ✅ Batch write (status=failed) | ✅ Update |
**Why skip Steps on success?**
- Steps are only needed for resume/retry operations
- If completed successfully, there's nothing to resume
- Reduces database writes and keeps Step table clean
- Reduces database writes and keeps Resume table clean
### Why Two Writes?
### Why Single Write?
| Scenario | What Happens | Data Safe? |
| ------------------ | ----------------------------------- | ---------- |
| Normal completion | `defer` triggers → Write 2 executes | ✅ |
| User clicks stop | `defer` triggers → Write 2 executes | ✅ |
| LLM timeout | `defer` triggers → Write 2 executes | ✅ |
| Tool failure | `defer` triggers → Write 2 executes | ✅ |
| Network disconnect | `defer` triggers → Write 2 executes | ✅ |
| ------------------ | --------------------------------- | ---------- |
| Normal completion | `defer` triggers → Write executes | ✅ |
| User clicks stop | `defer` triggers → Write executes | ✅ |
| LLM timeout | `defer` triggers → Write executes | ✅ |
| Tool failure | `defer` triggers → Write executes | ✅ |
| Network disconnect | `defer` triggers → Write executes | ✅ |
| Process crash | Service is down, user must retry | N/A |
**Note**: Process crash is a catastrophic failure handled at infrastructure level, not application level.
### Write Count Comparison
For a typical request: user input → hook_create → llm → tool → llm → hook_next → 5 messages
For a typical request: user input → hook_create → llm → tool → hook_next → 5 messages
| Strategy | Database Writes | Notes |
| ---------------------- | --------------- | ------------------ |
| ------------------------- | --------------- | --------------------- |
| Write per operation | 1 + 5 + 5 = 11 | One write per step |
| **Two-write strategy** | **2** | Entry + Exit only |
| **Single-write strategy** | **1** | Exit only (via defer) |
### Implementation
````go
func (ast *Assistant) Stream(ctx, inputMessages, options) {
// ========== Write 1: Entry ==========
userMsg := createUserMessage(ctx, inputMessages)
chatStore.SaveMessages(ctx.ChatID, []*Message{userMsg})
// ========== Memory Buffers ==========
messageBuffer := NewMessageBuffer()
stepBuffer := NewStepBuffer()
// Buffer user input message (not written yet)
userMsg := createUserMessage(ctx, inputMessages)
messageBuffer.Add(userMsg)
// Track current step for error handling
var currentStep *Step
defer func() {
// ========== Write 2: Exit (always executes) ==========
// ========== Single Write: Exit (always executes) ==========
// Determine final status for incomplete steps
finalStatus := "completed"
if ctx.IsInterrupted() {
@ -451,9 +604,13 @@ func (ast *Assistant) Stream(ctx, inputMessages, options) {
currentStep.Status = finalStatus
}
// Batch write all buffered data
// Batch write all buffered messages (user input + assistant responses)
chatStore.SaveMessages(ctx.ChatID, messageBuffer.GetAll())
chatStore.SaveSteps(stepBuffer.GetAll())
// Only save steps on error/interrupt (not on success)
if finalStatus != "completed" {
chatStore.SaveResume(stepBuffer.GetAll())
}
// Update token usage in OpenAPI request record
if ctx.RequestID != "" && completionResponse != nil {
@ -580,7 +737,6 @@ type Chat struct {
AssistantID string `json:"assistant_id"`
Mode string `json:"mode"`
Status string `json:"status"`
Preset bool `json:"preset"`
Public bool `json:"public"`
Share string `json:"share"` // "private" or "team"
Sort int `json:"sort"`
@ -639,6 +795,20 @@ type ChatFilter struct {
AssistantID string `json:"assistant_id,omitempty"`
Status string `json:"status,omitempty"`
Keywords string `json:"keywords,omitempty"`
// Time range filter
StartTime *time.Time `json:"start_time,omitempty"` // Filter chats after this time
EndTime *time.Time `json:"end_time,omitempty"` // Filter chats before this time
TimeField string `json:"time_field,omitempty"` // Field for time filter: "created_at" or "last_message_at" (default)
// Sorting
OrderBy string `json:"order_by,omitempty"` // Field to sort by (default: "last_message_at")
Order string `json:"order,omitempty"` // Sort order: "desc" (default) or "asc"
// Response format
GroupBy string `json:"group_by,omitempty"` // "time" for time-based groups, empty for flat list
// Pagination
Page int `json:"page,omitempty"`
PageSize int `json:"pagesize,omitempty"`
}
@ -648,18 +818,29 @@ type MessageFilter struct {
RequestID string `json:"request_id,omitempty"`
Role string `json:"role,omitempty"`
BlockID string `json:"block_id,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
Type string `json:"type,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// ChatList paginated response
// ChatList paginated response with time-based grouping
type ChatList struct {
Data []*Chat `json:"data"`
Groups []*ChatGroup `json:"groups,omitempty"` // Time-based groups for UI display
Page int `json:"page"`
PageSize int `json:"pagesize"`
PageCount int `json:"pagecount"`
Total int `json:"total"`
}
// ChatGroup represents a time-based group of chats
type ChatGroup struct {
Label string `json:"label"` // "Today", "Yesterday", "This Week", "This Month", "Earlier"
Key string `json:"key"` // "today", "yesterday", "this_week", "this_month", "earlier"
Chats []*Chat `json:"chats"` // Chats in this group
Count int `json:"count"` // Number of chats in group
}
```
## Usage Examples
@ -671,13 +852,12 @@ A typical conversation with various message types stored in `agent_message`:
```
User: "What's the weather in SF? Also show me a chart."
Timeline:
1. User sends multimodal input
2. Hook shows loading state
Timeline (user input → hook_create → llm → tool → hook_next):
1. User sends input
2. Create hook shows loading state
3. LLM thinks and calls tool
4. Tool returns result
5. LLM generates text response
6. Hook sends image chart
4. Tool executes and returns result
5. Next hook generates text response and image chart
```
**Stored Messages:**
@ -745,7 +925,7 @@ Timeline:
"sequence": 4
},
// 5. Tool result (role=assistant, type=text, with tool metadata)
// 5. Tool result from Next hook (role=assistant, type=text, with tool metadata)
{
"message_id": "msg_005",
"chat_id": "chat_123",
@ -753,39 +933,23 @@ Timeline:
"role": "assistant",
"type": "text",
"props": {
"content": "Weather data retrieved: 18°C, sunny, humidity 65%"
"content": "The weather in San Francisco is currently **18°C** and sunny with 65% humidity. Perfect weather for outdoor activities!"
},
"block_id": "B2",
"block_id": "B3",
"metadata": {
"tool_call_id": "call_weather_001",
"tool_name": "get_weather",
"is_tool_result": true
"tool_name": "get_weather"
},
"assistant_id": "weather_assistant",
"sequence": 5
},
// 6. LLM text response (role=assistant, type=text)
// 6. Chart image from Next hook (role=assistant, type=image)
{
"message_id": "msg_006",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "text",
"props": {
"content": "The weather in San Francisco is currently **18°C** and sunny with 65% humidity. Perfect weather for outdoor activities!"
},
"block_id": "B2",
"assistant_id": "weather_assistant",
"sequence": 6
},
// 7. Chart image from Next hook (role=assistant, type=image)
{
"message_id": "msg_007",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "image",
"props": {
"url": "https://charts.example.com/weather_sf.png",
@ -795,7 +959,7 @@ Timeline:
},
"block_id": "B3",
"assistant_id": "weather_assistant",
"sequence": 7
"sequence": 6
}
]
```
@ -904,13 +1068,54 @@ Multimedia content storage:
### 5. Load Chat History
```go
// Get chat list
// Example 1: Flat list (default)
chats, _ := chatStore.ListChats(ChatFilter{
UserID: "user123",
Status: "active",
OrderBy: "last_message_at",
Order: "desc",
Page: 1,
PageSize: 20,
})
// Response: chats.Data = [...], chats.Groups = nil
// Example 2: Grouped by time
chats, _ := chatStore.ListChats(ChatFilter{
UserID: "user123",
GroupBy: "time", // Enable time-based grouping
OrderBy: "last_message_at",
Order: "desc",
Page: 1,
PageSize: 20,
})
// Response includes time-based groups:
// chats.Groups = [
// { Key: "today", Label: "Today", Chats: [...], Count: 3 },
// { Key: "yesterday", Label: "Yesterday", Chats: [...], Count: 5 },
// { Key: "this_week", Label: "This Week", Chats: [...], Count: 8 },
// { Key: "this_month", Label: "This Month", Chats: [...], Count: 4 },
// { Key: "earlier", Label: "Earlier", Chats: [...], Count: 0 },
// ]
// Example 3: Filter by time range
startTime := time.Now().AddDate(0, 0, -7) // Last 7 days
chats, _ := chatStore.ListChats(ChatFilter{
UserID: "user123",
StartTime: &startTime,
TimeField: "last_message_at", // Filter by last message time
OrderBy: "last_message_at",
Order: "desc",
})
// Example 4: Filter specific date range
start := time.Date(2024, 12, 1, 0, 0, 0, 0, time.Local)
end := time.Date(2024, 12, 31, 23, 59, 59, 0, time.Local)
chats, _ := chatStore.ListChats(ChatFilter{
UserID: "user123",
StartTime: &start,
EndTime: &end,
TimeField: "created_at", // Filter by creation time
})
// Get messages for a chat
messages, _ := chatStore.GetMessages("chat_123", MessageFilter{
@ -1054,6 +1259,99 @@ return {
// 2. The delegated agent's Create hook can read: ctx.space.GetDel("choose_prompt")
```
## Concurrent Operations Storage
When an Agent makes parallel calls (e.g., multiple MCP tools, multiple sub-agents), messages use `block_id` and `thread_id` for grouping:
```
Main Agent concurrently calls 3 tasks:
├── Thread T1: Weather query (MCP)
├── Thread T2: News search (MCP)
├── Thread T3: Stock query (MCP)
└── Wait for all to complete, then summarize
```
**Stored Messages:**
```json
[
// All concurrent messages share the same block_id, different thread_id
// Messages may arrive in any order due to concurrency
// Thread T1: Weather result
{
"message_id": "msg_t1_001",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "text",
"props": { "content": "Weather in SF: 18°C, sunny" },
"block_id": "B1",
"thread_id": "T1",
"assistant_id": "main_assistant",
"sequence": 2
},
// Thread T2: News result
{
"message_id": "msg_t2_001",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "text",
"props": { "content": "Top news: AI breakthrough announced..." },
"block_id": "B1",
"thread_id": "T2",
"assistant_id": "main_assistant",
"sequence": 3
},
// Thread T3: Stock result
{
"message_id": "msg_t3_001",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "text",
"props": { "content": "AAPL: $185.50 (+1.2%)" },
"block_id": "B1",
"thread_id": "T3",
"assistant_id": "main_assistant",
"sequence": 4
},
// After all threads complete, main agent summarizes (new block)
{
"message_id": "msg_summary",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "text",
"props": {
"content": "Here's your daily briefing: The weather is great at 18°C..."
},
"block_id": "B2",
"thread_id": null,
"assistant_id": "main_assistant",
"sequence": 5
}
]
```
**Key Points:**
| Field | Concurrent Usage |
| ----------- | -------------------------------------------------- |
| `block_id` | Same for all parallel operations (B1) |
| `thread_id` | Different for each concurrent task (T1, T2, T3) |
| `sequence` | Reflects actual arrival order (may be interleaved) |
**Frontend Rendering:**
- Group messages by `block_id` for visual blocks
- Within a block, optionally group by `thread_id` to show parallel results
- Use `sequence` for chronological display
## Related Documents
- [OpenAPI Request Design](../../openapi/request/REQUEST_DESIGN.md) - Global request tracking, billing, rate limiting

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,8 @@ import (
var systemModels = map[string]string{
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
"__yao.agent.history": "yao/models/agent/history.mod.yao",
"__yao.agent.message": "yao/models/agent/message.mod.yao",
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
"__yao.attachment": "yao/models/attachment.mod.yao",
"__yao.audit": "yao/models/audit.mod.yao",
"__yao.config": "yao/models/config.mod.yao",

View file

@ -197,7 +197,8 @@ var testServer *http.Server = nil
var testSystemModels = map[string]string{
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
"__yao.agent.history": "yao/models/agent/history.mod.yao",
"__yao.agent.message": "yao/models/agent/message.mod.yao",
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
"__yao.attachment": "yao/models/attachment.mod.yao",
"__yao.audit": "yao/models/audit.mod.yao",
"__yao.config": "yao/models/config.mod.yao",

View file

@ -1,53 +1,103 @@
{
"name": "Chat",
"label": "Chat",
"description": "Chat table for storing chat metadata and information",
"description": "Chat session table for storing chat metadata and session information",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_chat", "comment": "Agent chat table" },
"table": { "name": "agent_chat", "comment": "Agent chat session table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "Chat ID",
"comment": "Unique chat identifier"
"label": "ID",
"comment": "Auto-increment primary key"
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Chat identifier",
"length": 200,
"comment": "Unique chat identifier",
"length": 64,
"nullable": false,
"unique": true,
"index": true
"unique": true
},
{
"name": "title",
"type": "string",
"label": "Title",
"comment": "Chat title",
"length": 200,
"length": 500,
"nullable": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant identifier",
"comment": "Associated assistant ID",
"length": 200,
"nullable": false,
"index": true
},
{
"name": "mode",
"type": "string",
"label": "Mode",
"comment": "Chat mode (default: chat)",
"length": 50,
"nullable": false,
"default": "chat"
},
{
"name": "status",
"type": "enum",
"label": "Status",
"comment": "Chat status",
"option": ["active", "archived"],
"default": "active",
"nullable": false,
"index": true
},
{
"name": "public",
"type": "boolean",
"label": "Public",
"comment": "Whether shared across all teams",
"default": false,
"nullable": false
},
{
"name": "share",
"type": "enum",
"label": "Share",
"comment": "Sharing scope",
"option": ["private", "team"],
"default": "private",
"nullable": false,
"index": true
},
{
"name": "sort",
"type": "integer",
"label": "Sort",
"comment": "Sort order for display",
"default": 9999
},
{
"name": "last_message_at",
"type": "datetime",
"label": "Last Message At",
"comment": "Timestamp of last message",
"nullable": true,
"index": true
},
{
"name": "silent",
"type": "boolean",
"label": "Silent Mode",
"comment": "Whether this is a silent chat",
"default": false,
"index": true
"name": "metadata",
"type": "json",
"label": "Metadata",
"comment": "Additional metadata",
"nullable": true
}
],
"relations": {
@ -57,20 +107,19 @@
"key": "assistant_id",
"foreign": "assistant_id"
},
"history": {
"messages": {
"type": "hasMany",
"model": "__yao.agent.history",
"model": "__yao.agent.message",
"key": "chat_id",
"foreign": "cid"
"foreign": "chat_id"
},
"resumes": {
"type": "hasMany",
"model": "__yao.agent.resume",
"key": "chat_id",
"foreign": "chat_id"
}
},
"indexes": [
{
"name": "idx_agent_chat_silent",
"columns": ["silent", "created_at"],
"type": "index",
"comment": "Index for silent mode filtering"
}
],
"option": { "timestamps": true, "soft_deletes": false, "permission": true }
"option": { "timestamps": true, "soft_deletes": true, "permission": true }
}

View file

@ -1,126 +0,0 @@
{
"name": "History",
"label": "Chat History",
"description": "Chat history table for storing detailed chat message information",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_history", "comment": "Agent chat history table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "Record ID",
"comment": "Unique record identifier"
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Chat identifier",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "name",
"type": "string",
"label": "Role Name",
"comment": "Role display name",
"length": 200,
"nullable": true
},
{
"name": "content",
"type": "text",
"label": "Message Content",
"comment": "Text content of the message",
"nullable": true
},
{
"name": "context",
"type": "json",
"label": "Context",
"comment": "Message context data",
"nullable": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant identifier",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "assistant_name",
"type": "string",
"label": "Assistant Name",
"comment": "Assistant display name",
"length": 200,
"nullable": true
},
{
"name": "assistant_avatar",
"type": "string",
"label": "Assistant Avatar",
"comment": "Assistant avatar URL",
"length": 200,
"nullable": true
},
{
"name": "mentions",
"type": "json",
"label": "Mentions",
"comment": "Mention information in the message",
"nullable": true
},
{
"name": "silent",
"type": "boolean",
"label": "Silent Mode",
"comment": "Whether this is a silent message",
"default": false,
"index": true
},
{
"name": "expired_at",
"type": "timestamp",
"label": "Expired At",
"comment": "Record expiration time",
"nullable": true,
"index": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
},
"user": {
"type": "hasOne",
"model": "__yao.user",
"key": "user_id",
"foreign": "user_id"
}
},
"indexes": [
{
"name": "idx_agent_history_expired",
"columns": ["expired_at", "silent"],
"type": "index",
"comment": "Index for expiration and cleanup"
}
],
"option": { "timestamps": true, "soft_deletes": false, "permission": true }
}

View file

@ -0,0 +1,134 @@
{
"name": "Message",
"label": "Message",
"description": "Chat message table for storing user-visible messages",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_message", "comment": "Agent chat message table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "ID",
"comment": "Auto-increment primary key"
},
{
"name": "message_id",
"type": "string",
"label": "Message ID",
"comment": "Unique message identifier",
"length": 64,
"nullable": false,
"unique": true
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Parent chat ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "request_id",
"type": "string",
"label": "Request ID",
"comment": "Request ID for grouping",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "role",
"type": "enum",
"label": "Role",
"comment": "Message role",
"option": ["user", "assistant"],
"nullable": false,
"index": true
},
{
"name": "type",
"type": "string",
"label": "Type",
"comment": "Message type (text, image, loading, tool_call, retrieval, etc.)",
"length": 50,
"nullable": false
},
{
"name": "props",
"type": "json",
"label": "Props",
"comment": "Message properties (content, url, etc.)",
"nullable": false
},
{
"name": "block_id",
"type": "string",
"label": "Block ID",
"comment": "Block grouping ID",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "thread_id",
"type": "string",
"label": "Thread ID",
"comment": "Thread grouping ID for concurrent operations",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant ID (join to get name/avatar)",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "sequence",
"type": "integer",
"label": "Sequence",
"comment": "Message order within chat",
"nullable": false
},
{
"name": "metadata",
"type": "json",
"label": "Metadata",
"comment": "Additional metadata (tool_call_id, tool_name, etc.)",
"nullable": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
}
},
"indexes": [
{
"name": "idx_msg_chat_seq",
"columns": ["chat_id", "sequence"],
"type": "index",
"comment": "Index for message ordering within chat"
}
],
"option": { "timestamps": true, "soft_deletes": false }
}

View file

@ -0,0 +1,170 @@
{
"name": "Resume",
"label": "Resume",
"description": "Resume table for storing execution state for resume/retry functionality",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_resume", "comment": "Agent resume/recovery table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "ID",
"comment": "Auto-increment primary key"
},
{
"name": "resume_id",
"type": "string",
"label": "Resume ID",
"comment": "Unique resume record identifier",
"length": 64,
"nullable": false,
"unique": true
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Parent chat ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "request_id",
"type": "string",
"label": "Request ID",
"comment": "Request ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant executing this step",
"length": 200,
"nullable": false,
"index": true
},
{
"name": "stack_id",
"type": "string",
"label": "Stack ID",
"comment": "Stack node ID for this execution",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "stack_parent_id",
"type": "string",
"label": "Stack Parent ID",
"comment": "Parent stack ID (for A2A calls)",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "stack_depth",
"type": "integer",
"label": "Stack Depth",
"comment": "Call depth (0=root, 1+=nested)",
"nullable": false,
"default": 0
},
{
"name": "type",
"type": "enum",
"label": "Type",
"comment": "Step type",
"option": [
"input",
"hook_create",
"llm",
"tool",
"hook_next",
"delegate"
],
"nullable": false,
"index": true
},
{
"name": "status",
"type": "enum",
"label": "Status",
"comment": "Resume status (only interrupted or failed are stored)",
"option": ["interrupted", "failed"],
"nullable": false,
"index": true
},
{
"name": "input",
"type": "json",
"label": "Input",
"comment": "Step input data",
"nullable": true
},
{
"name": "output",
"type": "json",
"label": "Output",
"comment": "Step output data (partial)",
"nullable": true
},
{
"name": "space_snapshot",
"type": "json",
"label": "Space Snapshot",
"comment": "Space data snapshot for recovery",
"nullable": true
},
{
"name": "error",
"type": "text",
"label": "Error",
"comment": "Error message if failed",
"nullable": true
},
{
"name": "sequence",
"type": "integer",
"label": "Sequence",
"comment": "Step order within request",
"nullable": false
},
{
"name": "metadata",
"type": "json",
"label": "Metadata",
"comment": "Additional metadata",
"nullable": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
}
},
"indexes": [
{
"name": "idx_resume_request_seq",
"columns": ["request_id", "sequence"],
"type": "index",
"comment": "Index for resume ordering within request"
}
],
"option": { "timestamps": true, "soft_deletes": false }
}