Add web search knowledge base, message tagging, and word-memory tip topics

Introduces websearch.py (Tavily) + knowledge.py to ground chat responses
in search results, tagging.py to tag user and assistant messages via LLM
and pick related topics for the Discord useless-tips job, and
word_memory.py to track recent conversation nouns (surfaced in the new
WordPool.vue sidebar) used to pick tip topics close to a related tag.
This commit is contained in:
2026-06-20 18:06:06 +09:00
parent 4e837d1419
commit 234ebba103
23 changed files with 946 additions and 39 deletions
+27 -7
View File
@@ -1,12 +1,14 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { fetchSessions, fetchMessages, deleteSession, streamChat, type SessionInfo } from '../api/client'
import { fetchSessions, fetchMessages, deleteSession, streamChat, type SessionInfo, type Source } from '../api/client'
export interface Message {
role: 'user' | 'assistant'
content: string
pending?: boolean
error?: boolean
tags?: string[]
sources?: Source[]
}
export interface Toast {
@@ -21,7 +23,7 @@ export const useChatStore = defineStore('chat', () => {
const currentSessionId = ref<string | null>(null)
const messages = ref<Message[]>([])
const isStreaming = ref(false)
const selectedModel = ref('gpt-5.4')
const selectedModel = ref('bedrock/jp.anthropic.claude-sonnet-4-6')
const toasts = ref<Toast[]>([])
const currentSession = computed(() =>
@@ -52,7 +54,7 @@ export const useChatStore = defineStore('chat', () => {
const records = await fetchMessages(sessionId)
messages.value = records
.filter((r) => r.role === 'user' || r.role === 'assistant')
.map((r) => ({ role: r.role as 'user' | 'assistant', content: r.content }))
.map((r) => ({ role: r.role as 'user' | 'assistant', content: r.content, tags: r.tags }))
} catch (e) {
pushToast(`メッセージ取得失敗: ${e instanceof Error ? e.message : e}`)
}
@@ -81,23 +83,27 @@ export const useChatStore = defineStore('chat', () => {
selectedModel.value,
(chunk) => {
if (chunk.type === 'session_id' && chunk.value) {
currentSessionId.value = chunk.value
currentSessionId.value = chunk.value as string
loadSessions().catch((e) => pushToast(`セッション更新失敗: ${e.message}`))
} else if (chunk.type === 'chunk' && chunk.value) {
messages.value[msgIdx].content += chunk.value
messages.value[msgIdx].content += chunk.value as string
} else if (chunk.type === 'sources' && chunk.value) {
messages.value[msgIdx].sources = chunk.value as Source[]
} else if (chunk.type === 'done') {
messages.value[msgIdx].pending = false
isStreaming.value = false
loadSessions().catch((e) => pushToast(`セッション更新失敗: ${e.message}`))
} else if (chunk.type === 'error') {
messages.value[msgIdx].content = chunk.value ?? 'バックエンドでエラーが発生しました。'
messages.value[msgIdx].content = (chunk.value as string) ?? 'バックエンドでエラーが発生しました。'
messages.value[msgIdx].pending = false
messages.value[msgIdx].error = true
isStreaming.value = false
}
},
(err) => {
messages.value[msgIdx].content = err.message
// 途中まで届いていた内容は残しつつ、エラーメッセージを付記する
const partial = messages.value[msgIdx].content
messages.value[msgIdx].content = partial ? `${partial}\n\n⚠️ ${err.message}` : err.message
messages.value[msgIdx].pending = false
messages.value[msgIdx].error = true
isStreaming.value = false
@@ -105,6 +111,19 @@ export const useChatStore = defineStore('chat', () => {
)
}
/** エラーになったアシスタント応答のひとつ前にあるユーザーメッセージを再送信する。 */
function retryMessage(assistantIdx: number): void {
if (isStreaming.value) return
const userIdx = assistantIdx - 1
const userMsg = messages.value[userIdx]
if (!userMsg || userMsg.role !== 'user') return
const text = userMsg.content
// 失敗したユーザーメッセージとエラー応答を取り除いてから送信し直す
messages.value.splice(userIdx, 2)
sendMessage(text)
}
return {
sessions,
currentSessionId,
@@ -119,5 +138,6 @@ export const useChatStore = defineStore('chat', () => {
selectSession,
removeSession,
sendMessage,
retryMessage,
}
})