Merge pull request #340 from lfnovo/fix/chat-model-selection
fix: chat model selection issues + Docker troubleshooting docs
This commit is contained in:
commit
bbe06df7e4
7 changed files with 215 additions and 24 deletions
|
|
@ -8,9 +8,12 @@ Click your problem:
|
||||||
|
|
||||||
### 1. ["Unable to connect to server" or blank page](#fix-connection-error)
|
### 1. ["Unable to connect to server" or blank page](#fix-connection-error)
|
||||||
### 2. [Container won't start or crashes](#fix-container-crash)
|
### 2. [Container won't start or crashes](#fix-container-crash)
|
||||||
### 3. [Works on server but not from my computer](#fix-remote-access)
|
### 3. [Quotes in environment variables](#fix-quotes-in-env)
|
||||||
### 4. [API or authentication errors](#fix-api-errors)
|
### 4. [SurrealDB configuration issues](#fix-surrealdb-config)
|
||||||
### 5. [Slow or timeout errors](#fix-performance)
|
### 5. [Network timeouts (slow connections / China)](#fix-network-timeouts)
|
||||||
|
### 6. [Works on server but not from my computer](#fix-remote-access)
|
||||||
|
### 7. [API or authentication errors](#fix-api-errors)
|
||||||
|
### 8. [Slow or timeout errors](#fix-performance)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -128,6 +131,9 @@ docker compose logs
|
||||||
| "Invalid API key" | Check OPENAI_API_KEY in environment variables |
|
| "Invalid API key" | Check OPENAI_API_KEY in environment variables |
|
||||||
| "Out of memory" | Increase Docker memory limit to 2GB+ in Docker Desktop settings |
|
| "Out of memory" | Increase Docker memory limit to 2GB+ in Docker Desktop settings |
|
||||||
| "No such file or directory" | Check volume paths exist and are accessible |
|
| "No such file or directory" | Check volume paths exist and are accessible |
|
||||||
|
| "'' is not a valid UrlScheme" | [Remove quotes from environment variables](#fix-quotes-in-env) |
|
||||||
|
| "There was a problem with authentication" | [Check SurrealDB configuration](#fix-surrealdb-config) |
|
||||||
|
| Worker/API crashes on startup | [Check network timeouts](#fix-network-timeouts) |
|
||||||
|
|
||||||
**Quick reset:**
|
**Quick reset:**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -135,6 +141,127 @@ docker compose down -v
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<a name="fix-quotes-in-env"></a>
|
||||||
|
### Fix: Quotes in Environment Variables
|
||||||
|
|
||||||
|
**Symptom:** Error `'' is not a valid UrlScheme` or database connection fails with empty URL.
|
||||||
|
|
||||||
|
**Cause:** Docker Compose interprets quotes literally. If you have quotes around values in your `docker-compose.yml` or `.env` file, they become part of the value.
|
||||||
|
|
||||||
|
❌ **Wrong** (quotes become part of the value):
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SURREAL_URL="ws://localhost:8000/rpc"
|
||||||
|
- SURREAL_USER="root"
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Also wrong** in `.env` files:
|
||||||
|
```env
|
||||||
|
SURREAL_URL="ws://localhost:8000/rpc"
|
||||||
|
SURREAL_USER="root"
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Correct** (no quotes):
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SURREAL_URL=ws://localhost:8000/rpc
|
||||||
|
- SURREAL_USER=root
|
||||||
|
- SURREAL_PASSWORD=root
|
||||||
|
- SURREAL_NAMESPACE=open_notebook
|
||||||
|
- SURREAL_DATABASE=production
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Correct** `.env` file:
|
||||||
|
```env
|
||||||
|
SURREAL_URL=ws://localhost:8000/rpc
|
||||||
|
SURREAL_USER=root
|
||||||
|
```
|
||||||
|
|
||||||
|
After fixing, restart:
|
||||||
|
```bash
|
||||||
|
docker compose down && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="fix-surrealdb-config"></a>
|
||||||
|
### Fix: SurrealDB Configuration Issues
|
||||||
|
|
||||||
|
#### Single Container Already Has SurrealDB
|
||||||
|
|
||||||
|
**Symptom:** Authentication errors or connection issues when using `v1-latest-single` with an external SurrealDB.
|
||||||
|
|
||||||
|
**Cause:** The `-single` image already includes SurrealDB. You don't need to run a separate SurrealDB container.
|
||||||
|
|
||||||
|
❌ **Wrong** - running separate SurrealDB with single container:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
surrealdb:
|
||||||
|
image: surrealdb/surrealdb:latest # Not needed!
|
||||||
|
|
||||||
|
open_notebook:
|
||||||
|
image: lfnovo/open_notebook:v1-latest-single
|
||||||
|
environment:
|
||||||
|
- SURREAL_URL=ws://surrealdb:8000/rpc # Wrong!
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Correct** - single container uses built-in SurrealDB:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
open_notebook:
|
||||||
|
image: lfnovo/open_notebook:v1-latest-single
|
||||||
|
environment:
|
||||||
|
- SURREAL_URL=ws://localhost:8000/rpc # Uses internal DB
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you want a separate SurrealDB**, use the `v1-latest` image (without `-single`) instead.
|
||||||
|
|
||||||
|
#### SurrealDB Version Compatibility
|
||||||
|
|
||||||
|
**Symptom:** Various database errors, authentication failures, or unexpected behavior.
|
||||||
|
|
||||||
|
**Cause:** Open Notebook currently supports **SurrealDB v2.x only**. SurrealDB v3 (alpha) is not yet supported.
|
||||||
|
|
||||||
|
✅ **Supported versions:**
|
||||||
|
```yaml
|
||||||
|
# Use v2.x
|
||||||
|
image: surrealdb/surrealdb:v2.1.4
|
||||||
|
image: surrealdb/surrealdb:v2 # Latest v2
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Not supported yet:**
|
||||||
|
```yaml
|
||||||
|
# Don't use v3 alpha
|
||||||
|
image: surrealdb/surrealdb:v3.0.0-alpha.17
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="fix-network-timeouts"></a>
|
||||||
|
### Fix: Network Timeouts (Slow Connections / China)
|
||||||
|
|
||||||
|
**Symptom:** Container crashes on startup with `exit status 1`, worker enters FATAL state, or pip/uv dependency downloads fail.
|
||||||
|
|
||||||
|
**Cause:** The container downloads Python dependencies on first startup. Slow networks or restricted access (especially in China) can cause timeouts.
|
||||||
|
|
||||||
|
✅ **Fix:** Add timeout and mirror configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
open_notebook:
|
||||||
|
image: lfnovo/open_notebook:v1-latest-single
|
||||||
|
environment:
|
||||||
|
# Increase download timeout to 10 minutes (default is 30s)
|
||||||
|
- UV_HTTP_TIMEOUT=600
|
||||||
|
|
||||||
|
# For users in China - use mirror
|
||||||
|
- UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
- PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative mirrors for China:**
|
||||||
|
- Tsinghua: `https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||||
|
- Aliyun: `https://mirrors.aliyun.com/pypi/simple/`
|
||||||
|
- Huawei: `https://repo.huaweicloud.com/repository/pypi/simple`
|
||||||
|
|
||||||
|
**Note:** First startup may take several minutes while dependencies are downloaded. Subsequent startups will be faster.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a name="fix-remote-access"></a>
|
<a name="fix-remote-access"></a>
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,8 @@ export function ChatColumn({ notebookId, contextSelections }: ChatColumnProps) {
|
||||||
isStreaming={chat.isSending}
|
isStreaming={chat.isSending}
|
||||||
contextIndicators={null}
|
contextIndicators={null}
|
||||||
onSendMessage={(message, modelOverride) => chat.sendMessage(message, modelOverride)}
|
onSendMessage={(message, modelOverride) => chat.sendMessage(message, modelOverride)}
|
||||||
modelOverride={chat.currentSession?.model_override ?? undefined}
|
modelOverride={chat.currentSession?.model_override ?? chat.pendingModelOverride ?? undefined}
|
||||||
onModelChange={(model) => {
|
onModelChange={(model) => chat.setModelOverride(model ?? null)}
|
||||||
if (chat.currentSessionId) {
|
|
||||||
chat.updateSession(chat.currentSessionId, { model_override: model ?? null })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sessions={chat.sessions}
|
sessions={chat.sessions}
|
||||||
currentSessionId={chat.currentSessionId}
|
currentSessionId={chat.currentSessionId}
|
||||||
onCreateSession={(title) => chat.createSession(title)}
|
onCreateSession={(title) => chat.createSession(title)}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
Edit2,
|
Edit2,
|
||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
Clock
|
Clock
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { BaseChatSession } from '@/lib/types/api'
|
import { BaseChatSession } from '@/lib/types/api'
|
||||||
|
import { useModels } from '@/lib/hooks/use-models'
|
||||||
|
|
||||||
interface SessionManagerProps {
|
interface SessionManagerProps {
|
||||||
sessions: BaseChatSession[]
|
sessions: BaseChatSession[]
|
||||||
|
|
@ -53,6 +54,16 @@ export function SessionManager({
|
||||||
const [editTitle, setEditTitle] = useState('')
|
const [editTitle, setEditTitle] = useState('')
|
||||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { data: models } = useModels()
|
||||||
|
|
||||||
|
// Helper to get model name from ID
|
||||||
|
const getModelName = useMemo(() => {
|
||||||
|
return (modelId: string) => {
|
||||||
|
const model = models?.find(m => m.id === modelId)
|
||||||
|
return model?.name || 'Custom Model'
|
||||||
|
}
|
||||||
|
}, [models])
|
||||||
|
|
||||||
const handleCreateSession = () => {
|
const handleCreateSession = () => {
|
||||||
if (newSessionTitle.trim()) {
|
if (newSessionTitle.trim()) {
|
||||||
onCreateSession(newSessionTitle.trim())
|
onCreateSession(newSessionTitle.trim())
|
||||||
|
|
@ -211,14 +222,14 @@ export function SessionManager({
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
{formatDistanceToNow(new Date(session.created), { addSuffix: true })}
|
{formatDistanceToNow(new Date(session.created), { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
{session.message_count && session.message_count > 0 && (
|
{session.message_count != null && session.message_count > 0 && (
|
||||||
<Badge variant="secondary" className="mt-2 text-xs">
|
<Badge variant="secondary" className="mt-2 text-xs">
|
||||||
{session.message_count} messages
|
{session.message_count} messages
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{session.model_override && (
|
{session.model_override && (
|
||||||
<Badge variant="outline" className="mt-2 ml-2 text-xs">
|
<Badge variant="outline" className="mt-2 ml-2 text-xs">
|
||||||
{session.model_override}
|
{getModelName(session.model_override)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { FileText } from 'lucide-react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import { useInsight } from '@/lib/hooks/use-insights'
|
import { useInsight } from '@/lib/hooks/use-insights'
|
||||||
|
import { useModalManager } from '@/lib/hooks/use-modal-manager'
|
||||||
|
|
||||||
interface SourceInsightDialogProps {
|
interface SourceInsightDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -14,10 +17,13 @@ interface SourceInsightDialogProps {
|
||||||
insight_type?: string
|
insight_type?: string
|
||||||
content?: string
|
content?: string
|
||||||
created?: string
|
created?: string
|
||||||
|
source_id?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsightDialogProps) {
|
export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsightDialogProps) {
|
||||||
|
const { openModal } = useModalManager()
|
||||||
|
|
||||||
// Ensure insight ID has 'source_insight:' prefix for API calls
|
// Ensure insight ID has 'source_insight:' prefix for API calls
|
||||||
const insightIdWithPrefix = insight?.id
|
const insightIdWithPrefix = insight?.id
|
||||||
? (insight.id.includes(':') ? insight.id : `source_insight:${insight.id}`)
|
? (insight.id.includes(':') ? insight.id : `source_insight:${insight.id}`)
|
||||||
|
|
@ -28,17 +34,39 @@ export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsig
|
||||||
// Use fetched data if available, otherwise fall back to passed-in insight
|
// Use fetched data if available, otherwise fall back to passed-in insight
|
||||||
const displayInsight = fetchedInsight ?? insight
|
const displayInsight = fetchedInsight ?? insight
|
||||||
|
|
||||||
|
// Get source_id from fetched data (preferred) or passed-in insight
|
||||||
|
const sourceId = fetchedInsight?.source_id ?? insight?.source_id
|
||||||
|
|
||||||
|
const handleViewSource = () => {
|
||||||
|
if (sourceId) {
|
||||||
|
openModal('source', sourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] flex flex-col">
|
<DialogContent className="sm:max-w-3xl max-h-[90vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center justify-between gap-2">
|
<DialogTitle className="flex items-center justify-between gap-2">
|
||||||
<span>Source Insight</span>
|
<span>Source Insight</span>
|
||||||
{displayInsight?.insight_type && (
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="text-xs uppercase">
|
{displayInsight?.insight_type && (
|
||||||
{displayInsight.insight_type}
|
<Badge variant="outline" className="text-xs uppercase">
|
||||||
</Badge>
|
{displayInsight.insight_type}
|
||||||
)}
|
</Badge>
|
||||||
|
)}
|
||||||
|
{sourceId && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleViewSource}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<FileText className="h-3 w-3" />
|
||||||
|
View Source
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
const [tokenCount, setTokenCount] = useState<number>(0)
|
const [tokenCount, setTokenCount] = useState<number>(0)
|
||||||
const [charCount, setCharCount] = useState<number>(0)
|
const [charCount, setCharCount] = useState<number>(0)
|
||||||
|
// Pending model override for when user changes model before a session exists
|
||||||
|
const [pendingModelOverride, setPendingModelOverride] = useState<string | null>(null)
|
||||||
|
|
||||||
// Fetch sessions for this notebook
|
// Fetch sessions for this notebook
|
||||||
const {
|
const {
|
||||||
|
|
@ -176,10 +178,14 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
: message
|
: message
|
||||||
const newSession = await chatApi.createSession({
|
const newSession = await chatApi.createSession({
|
||||||
notebook_id: notebookId,
|
notebook_id: notebookId,
|
||||||
title: defaultTitle
|
title: defaultTitle,
|
||||||
|
// Include pending model override when creating session
|
||||||
|
model_override: pendingModelOverride ?? undefined
|
||||||
})
|
})
|
||||||
sessionId = newSession.id
|
sessionId = newSession.id
|
||||||
setCurrentSessionId(sessionId)
|
setCurrentSessionId(sessionId)
|
||||||
|
// Clear pending model override now that it's applied to the session
|
||||||
|
setPendingModelOverride(null)
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: QUERY_KEYS.notebookChatSessions(notebookId)
|
queryKey: QUERY_KEYS.notebookChatSessions(notebookId)
|
||||||
})
|
})
|
||||||
|
|
@ -226,6 +232,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
notebookId,
|
notebookId,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
currentSession,
|
currentSession,
|
||||||
|
pendingModelOverride,
|
||||||
buildContext,
|
buildContext,
|
||||||
refetchCurrentSession,
|
refetchCurrentSession,
|
||||||
queryClient
|
queryClient
|
||||||
|
|
@ -257,6 +264,20 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
return deleteSessionMutation.mutate(sessionId)
|
return deleteSessionMutation.mutate(sessionId)
|
||||||
}, [deleteSessionMutation])
|
}, [deleteSessionMutation])
|
||||||
|
|
||||||
|
// Set model override - handles both existing sessions and pending state
|
||||||
|
const setModelOverride = useCallback((model: string | null) => {
|
||||||
|
if (currentSessionId) {
|
||||||
|
// Session exists - update it directly
|
||||||
|
updateSessionMutation.mutate({
|
||||||
|
sessionId: currentSessionId,
|
||||||
|
data: { model_override: model }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// No session yet - store as pending
|
||||||
|
setPendingModelOverride(model)
|
||||||
|
}
|
||||||
|
}, [currentSessionId, updateSessionMutation])
|
||||||
|
|
||||||
// Update token/char counts when context selections change
|
// Update token/char counts when context selections change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateContextCounts = async () => {
|
const updateContextCounts = async () => {
|
||||||
|
|
@ -279,6 +300,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
loadingSessions,
|
loadingSessions,
|
||||||
tokenCount,
|
tokenCount,
|
||||||
charCount,
|
charCount,
|
||||||
|
pendingModelOverride,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
createSession,
|
createSession,
|
||||||
|
|
@ -286,6 +308,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
||||||
deleteSession,
|
deleteSession,
|
||||||
switchSession,
|
switchSession,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
setModelOverride,
|
||||||
refetchSessions
|
refetchSessions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ T = TypeVar("T", bound="ObjectModel")
|
||||||
class ObjectModel(BaseModel):
|
class ObjectModel(BaseModel):
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
table_name: ClassVar[str] = ""
|
table_name: ClassVar[str] = ""
|
||||||
|
nullable_fields: ClassVar[set[str]] = set() # Fields that can be saved as None
|
||||||
created: Optional[datetime] = None
|
created: Optional[datetime] = None
|
||||||
updated: Optional[datetime] = None
|
updated: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
@ -167,7 +168,11 @@ class ObjectModel(BaseModel):
|
||||||
|
|
||||||
def _prepare_save_data(self) -> Dict[str, Any]:
|
def _prepare_save_data(self) -> Dict[str, Any]:
|
||||||
data = self.model_dump()
|
data = self.model_dump()
|
||||||
return {key: value for key, value in data.items() if value is not None}
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in data.items()
|
||||||
|
if value is not None or key in self.__class__.nullable_fields
|
||||||
|
}
|
||||||
|
|
||||||
async def delete(self) -> bool:
|
async def delete(self) -> bool:
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,7 @@ class Note(ObjectModel):
|
||||||
|
|
||||||
class ChatSession(ObjectModel):
|
class ChatSession(ObjectModel):
|
||||||
table_name: ClassVar[str] = "chat_session"
|
table_name: ClassVar[str] = "chat_session"
|
||||||
|
nullable_fields: ClassVar[set[str]] = {"model_override"}
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
model_override: Optional[str] = None
|
model_override: Optional[str] = None
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue