feat: add @tag model override support and remove Qdrant dependencies
- Add model-tags parser for @Tag syntax in chat messages - Support Anthropic models (Sonnet, Haiku, Opus) via @tag - Remove Qdrant vector database from infrastructure and configs - Simplify license model config to use null fallbacks - Add greeting stream after model switch via @tag - Fix protobuf field names to camelCase for v7 compatibility - Add 429 rate limit retry logic with exponential backoff - Remove RAG references from agent harness documentation
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import DetailsEditDialog from './DetailsEditDialog.vue'
|
||||
import ResearchViewDialog from './ResearchViewDialog.vue'
|
||||
import { useIndicatorStore } from '../stores/indicators'
|
||||
import { useIndicatorTypesStore } from '../stores/indicatorTypes'
|
||||
import { useStrategyTypesStore } from '../stores/strategyTypes'
|
||||
import { useResearchTypesStore } from '../stores/researchTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
category: 'indicator' | 'strategy' | 'research'
|
||||
@@ -16,8 +20,13 @@ const editingName = ref('')
|
||||
const viewDialogVisible = ref(false)
|
||||
const viewingName = ref('')
|
||||
|
||||
const deleteDialogVisible = ref(false)
|
||||
const deletingRow = ref<{ id: string; display_name: string } | null>(null)
|
||||
|
||||
const indicatorStore = useIndicatorStore()
|
||||
const indicatorTypesStore = useIndicatorTypesStore()
|
||||
const strategyTypesStore = useStrategyTypesStore()
|
||||
const researchTypesStore = useResearchTypesStore()
|
||||
|
||||
function openEdit(name: string) {
|
||||
editingName.value = name
|
||||
@@ -50,6 +59,25 @@ function addToChart(pandasTaName: string, displayName: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function requestDelete(row: { id: string; display_name: string }) {
|
||||
deletingRow.value = row
|
||||
deleteDialogVisible.value = true
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (!deletingRow.value) return
|
||||
const id = deletingRow.value.id
|
||||
if (props.category === 'indicator') {
|
||||
indicatorTypesStore.removeType(id)
|
||||
} else if (props.category === 'strategy') {
|
||||
strategyTypesStore.removeType(id)
|
||||
} else {
|
||||
researchTypesStore.removeType(id)
|
||||
}
|
||||
deleteDialogVisible.value = false
|
||||
deletingRow.value = null
|
||||
}
|
||||
|
||||
function onUpdated(_payload: { category: string; name: string; success: boolean; error?: string }) {
|
||||
}
|
||||
</script>
|
||||
@@ -63,6 +91,7 @@ function onUpdated(_payload: { category: string; name: string; success: boolean;
|
||||
<button class="edit-btn" @click="openEdit(row.display_name)">Spec</button>
|
||||
<button v-if="category === 'research'" class="view-btn" @click="openView(row.display_name)">Result</button>
|
||||
<button v-if="category === 'indicator'" class="use-btn" @click="addToChart(row.id, row.display_name)">Use</button>
|
||||
<button class="delete-btn" title="Delete" @click.stop="requestDelete(row)"><i class="pi pi-trash" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,6 +108,21 @@ function onUpdated(_payload: { category: string; name: string; success: boolean;
|
||||
:name="editingName"
|
||||
@updated="onUpdated"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="deleteDialogVisible"
|
||||
:modal="true"
|
||||
:closable="true"
|
||||
:header="`Delete ${category}?`"
|
||||
:style="{ width: '360px' }"
|
||||
class="delete-confirm-dialog"
|
||||
>
|
||||
<p class="delete-msg">Delete <strong>{{ deletingRow?.display_name }}</strong>? This cannot be undone.</p>
|
||||
<template #footer>
|
||||
<Button label="Cancel" size="small" outlined @click="deleteDialogVisible = false" />
|
||||
<Button label="Delete" size="small" severity="danger" @click="confirmDelete" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -181,4 +225,33 @@ function onUpdated(_payload: { category: string; name: string; success: boolean;
|
||||
border-color: #4a9eca;
|
||||
color: #4a9eca;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #e06c6c;
|
||||
}
|
||||
|
||||
.delete-msg {
|
||||
color: #dbdbdb;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.delete-msg strong {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,6 +41,12 @@ const messages = ref<any[]>([])
|
||||
const messagesLoaded = ref(false)
|
||||
const isConnected = wsManager.isConnected
|
||||
|
||||
// Model tag state
|
||||
const availableModelTags = ref<string[]>([])
|
||||
const currentModelTag = ref<string | null>(null)
|
||||
// Content typed after @Tag, held until model_switched arrives to re-insert as first user message
|
||||
let pendingModelSwitchContent = ''
|
||||
|
||||
// Reactive rooms that update based on WebSocket connection and agent processing state
|
||||
const rooms = computed(() => [{
|
||||
roomId: SESSION_ID,
|
||||
@@ -48,7 +54,8 @@ const rooms = computed(() => [{
|
||||
avatar: null,
|
||||
users: [
|
||||
{ _id: CURRENT_USER_ID, username: 'You' },
|
||||
{ _id: AGENT_ID, username: 'AI Agent' }
|
||||
{ _id: AGENT_ID, username: 'AI Agent' },
|
||||
...availableModelTags.value.map(tag => ({ _id: `model-${tag.toLowerCase().replace(/[^a-z0-9]/g, '-')}`, username: tag })),
|
||||
],
|
||||
unreadCount: 0,
|
||||
typingUsers: isAgentProcessing.value ? [AGENT_ID] : []
|
||||
@@ -153,6 +160,63 @@ const streamingImages = ref<any[]>([])
|
||||
const handleMessage = (data: WebSocketMessage) => {
|
||||
console.log('[ChatPanel] Received message:', data)
|
||||
|
||||
if (data.type === 'connected') {
|
||||
if (Array.isArray(data.modelTags)) {
|
||||
availableModelTags.value = data.modelTags
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'model_switched') {
|
||||
// Reset all streaming state from previous conversation
|
||||
currentStreamingMessageId = null
|
||||
toolCallMessageId = null
|
||||
lastSentMessageId = null
|
||||
streamingBuffer = ''
|
||||
streamingImages.value = []
|
||||
subagentContentMap = new Map()
|
||||
currentModelTag.value = data.tag ?? null
|
||||
|
||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||
const date = new Date().toLocaleDateString()
|
||||
|
||||
// System notice at the top of the new conversation
|
||||
const systemMsg = {
|
||||
_id: generateMessageId(),
|
||||
content: `Switched to ${data.tag} — conversation history cleared`,
|
||||
system: true,
|
||||
timestamp,
|
||||
date,
|
||||
}
|
||||
|
||||
const newMessages: any[] = [systemMsg]
|
||||
|
||||
// Re-add the user's first message (content after the @Tag)
|
||||
const userContent: string = pendingModelSwitchContent || data.rest || ''
|
||||
if (userContent) {
|
||||
const userMsgId = generateMessageId()
|
||||
lastSentMessageId = userMsgId
|
||||
newMessages.push({
|
||||
_id: userMsgId,
|
||||
content: userContent,
|
||||
senderId: CURRENT_USER_ID,
|
||||
timestamp,
|
||||
date,
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: false,
|
||||
files: [],
|
||||
})
|
||||
}
|
||||
|
||||
messages.value = newMessages
|
||||
pendingModelSwitchContent = ''
|
||||
|
||||
// Show thinking bubble now that the message list is set up
|
||||
addToolCallBubble('Thinking...')
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'conversation_history') {
|
||||
messages.value = (data.messages as any[]).map((m: any) => {
|
||||
const ts = new Date(m.timestamp / 1000) // microseconds → ms
|
||||
@@ -189,7 +253,7 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'subagent_chunk') {
|
||||
if (data.type === 'subagent_chunk' || data.type === 'subagent_thinking') {
|
||||
appendSubagentChunk(data.agentName, data.content)
|
||||
return
|
||||
}
|
||||
@@ -363,7 +427,17 @@ const sendMessage = async (event: any) => {
|
||||
// Extract data from CustomEvent.detail[0]
|
||||
const data = event.detail?.[0] || event
|
||||
|
||||
const content = data.content || ''
|
||||
// Convert any model @-tags from vue-advanced-chat's <usertag> encoding back to @TagName
|
||||
const rawContent: string = data.content || ''
|
||||
const usersTag: any[] = data.usersTag || []
|
||||
let content = rawContent
|
||||
let modelSwitchUser: any = null
|
||||
for (const user of usersTag) {
|
||||
if (typeof user._id === 'string' && user._id.startsWith('model-')) {
|
||||
content = content.replace(`<usertag>${user._id}</usertag>`, `@${user.username}`)
|
||||
if (!modelSwitchUser) modelSwitchUser = user
|
||||
}
|
||||
}
|
||||
const files = data.files
|
||||
const roomId = data.roomId
|
||||
|
||||
@@ -411,7 +485,24 @@ const sendMessage = async (event: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Add user message to UI
|
||||
// Send to backend via WebSocket
|
||||
const wsMessage = {
|
||||
type: 'agent_user_message',
|
||||
session_id: roomId || SESSION_ID,
|
||||
content: content,
|
||||
attachments: attachments
|
||||
}
|
||||
|
||||
if (modelSwitchUser) {
|
||||
// Model switch: store content-after-tag for model_switched handler to re-insert.
|
||||
// Don't add a user message bubble now — model_switched will set up the full initial state.
|
||||
pendingModelSwitchContent = content.replace(`@${modelSwitchUser.username}`, '').trim()
|
||||
wsManager.send(wsMessage)
|
||||
isAgentProcessing.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// Normal message: add to UI immediately
|
||||
const userMessage = {
|
||||
_id: messageId,
|
||||
content: content,
|
||||
@@ -425,14 +516,6 @@ const sendMessage = async (event: any) => {
|
||||
}
|
||||
|
||||
messages.value = [...messages.value, userMessage]
|
||||
|
||||
// Send to backend via WebSocket
|
||||
const wsMessage = {
|
||||
type: 'agent_user_message',
|
||||
session_id: roomId || SESSION_ID,
|
||||
content: content,
|
||||
attachments: attachments
|
||||
}
|
||||
wsManager.send(wsMessage)
|
||||
|
||||
// Track this message so the agent_chunk handler can mark it seen
|
||||
@@ -692,8 +775,12 @@ onUnmounted(() => {
|
||||
<span class="workspace-loading-message">{{ channelStore.statusMessage || 'Connecting...' }}</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="currentModelTag" class="model-tag-badge">
|
||||
Model: {{ currentModelTag }}
|
||||
</div>
|
||||
|
||||
<vue-advanced-chat
|
||||
v-else
|
||||
:height="chatHeight"
|
||||
:current-user-id="CURRENT_USER_ID"
|
||||
:rooms="JSON.stringify(rooms)"
|
||||
@@ -751,6 +838,7 @@ onUnmounted(() => {
|
||||
</svg>
|
||||
</div>
|
||||
</vue-advanced-chat>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -765,6 +853,20 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.model-tag-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
font-size: 0.7rem;
|
||||
color: #089981;
|
||||
background: rgba(8, 153, 129, 0.12);
|
||||
border: 1px solid rgba(8, 153, 129, 0.3);
|
||||
border-radius: 4px;
|
||||
padding: 2px 7px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.workspace-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -13,5 +13,6 @@ export interface CustomIndicatorType {
|
||||
|
||||
export const useIndicatorTypesStore = defineStore('indicator_types', () => {
|
||||
const types = ref<Record<string, CustomIndicatorType>>({})
|
||||
return { types }
|
||||
const removeType = (id: string) => { delete types.value[id] }
|
||||
return { types, removeType }
|
||||
})
|
||||
|
||||
@@ -10,5 +10,6 @@ export interface ResearchType {
|
||||
|
||||
export const useResearchTypesStore = defineStore('research_types', () => {
|
||||
const types = ref<Record<string, ResearchType>>({})
|
||||
return { types }
|
||||
const removeType = (id: string) => { delete types.value[id] }
|
||||
return { types, removeType }
|
||||
})
|
||||
|
||||
@@ -10,5 +10,6 @@ export interface StrategyType {
|
||||
|
||||
export const useStrategyTypesStore = defineStore('strategy_types', () => {
|
||||
const types = ref<Record<string, StrategyType>>({})
|
||||
return { types }
|
||||
const removeType = (id: string) => { delete types.value[id] }
|
||||
return { types, removeType }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user