add submodule: kvstore

This commit is contained in:
2026-04-14 04:48:40 +00:00
parent 8d8130bbf0
commit a1254d5723
15 changed files with 68 additions and 227 deletions

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@ docs
.workspace.codex .workspace.codex
.env .env
.vscode .vscode
.codex

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "third_party/kvstore"]
path = third_party/kvstore
url = http://gitlab.0voice.com/lianyiheng/9.1-kvstore.git

View File

@@ -8,23 +8,8 @@ protoc \
``` ```
```shell ```shell
# ai-chat-web git submodule update --init --recursive
# ai-chat-backend ./script/run.sh
docker build -t ai-chat-backend:1.0.0 .
# ai-chat-service
docker build -t ai-chat-service:1.0.0 .
# chatgpt-web-frontend
HUSKY=0 pnpm bootstrap
pnpm dev
docker build -t chatgpt-web-frontend:1.0.0 .
# tokenizer
docker build -t tokenizer:1.0.0 .
# keywords-filter
docker build -t keywords-filter:1.0.0 .
``` ```

View File

@@ -17,9 +17,9 @@ chat:
bot_desc: "你是一个AI助手我需要你模拟一名资深的软件工程师来回答我的问题" bot_desc: "你是一个AI助手我需要你模拟一名资深的软件工程师来回答我的问题"
min_response_tokens: 600 min_response_tokens: 600
redis: redis:
host: "redis" host: "host.docker.internal"
port: 6379 port: 8888
pwd: "" pwd: "123456"
dependOn: dependOn:
sensitive: sensitive:
address: "sensitive-filter:50053" address: "sensitive-filter:50053"

View File

@@ -74,6 +74,8 @@ services:
- .env - .env
volumes: volumes:
- ./configs/ai-chat-service.yaml:/app/config.yaml:ro - ./configs/ai-chat-service.yaml:/app/config.yaml:ro
extra_hosts:
- "host.docker.internal:host-gateway"
ports: ports:
- "50055:50055" - "50055:50055"
depends_on: depends_on:

View File

@@ -17,9 +17,9 @@ chat:
bot_desc: "你是一个AI助手我需要你模拟一名资深的软件工程师来回答我的问题" bot_desc: "你是一个AI助手我需要你模拟一名资深的软件工程师来回答我的问题"
min_response_tokens: 600 min_response_tokens: 600
redis: redis:
host: "127.0.0.1" host: "host.docker.internal"
port: 8888 port: 8888
pwd: "" pwd: "123456"
dependOn: dependOn:
sensitive: sensitive:
address: "sensitive-filter:50053" address: "sensitive-filter:50053"

View File

@@ -50,6 +50,8 @@ export default {
showRawText: 'Show as raw text', showRawText: 'Show as raw text',
sourceSemantic: 'Semantic Match', sourceSemantic: 'Semantic Match',
sourceLlm: 'LLM Output', sourceLlm: 'LLM Output',
inputTokensShort: 'Input',
outputTokensShort: 'Output',
inputTokens: 'Input {count}', inputTokens: 'Input {count}',
outputTokens: 'Output {count}', outputTokens: 'Output {count}',
sessionTokens: 'Session {count} tokens', sessionTokens: 'Session {count} tokens',

View File

@@ -50,6 +50,8 @@ export default {
showRawText: '显示原文', showRawText: '显示原文',
sourceSemantic: '语义匹配', sourceSemantic: '语义匹配',
sourceLlm: '大模型输出', sourceLlm: '大模型输出',
inputTokensShort: '输入',
outputTokensShort: '输出',
inputTokens: 'Input {count}', inputTokens: 'Input {count}',
outputTokens: 'Output {count}', outputTokens: 'Output {count}',
sessionTokens: '本轮消耗 {count} tokens', sessionTokens: '本轮消耗 {count} tokens',

View File

@@ -50,6 +50,8 @@ export default {
showRawText: '顯示原文', showRawText: '顯示原文',
sourceSemantic: '語義匹配', sourceSemantic: '語義匹配',
sourceLlm: '大模型輸出', sourceLlm: '大模型輸出',
inputTokensShort: '輸入',
outputTokensShort: '輸出',
inputTokens: 'Input {count}', inputTokens: 'Input {count}',
outputTokens: 'Output {count}', outputTokens: 'Output {count}',
sessionTokens: '本輪消耗 {count} tokens', sessionTokens: '本輪消耗 {count} tokens',

View File

@@ -54,28 +54,43 @@ const sourceLabel = computed(() => {
const sourceClass = computed(() => { const sourceClass = computed(() => {
switch (props.messageMeta?.source) { switch (props.messageMeta?.source) {
case 'semantic_match': case 'semantic_match':
return 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/60 dark:bg-sky-950/40 dark:text-sky-300'
case 'llm': case 'llm':
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300' return 'border-[#d8e2eb] bg-[#f7fafc] text-[#52606d] dark:border-neutral-700 dark:bg-[#1a1d22] dark:text-neutral-200'
default: default:
return 'border-[#d0d7de] bg-white text-[#57606a] dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300' return 'border-[#d0d7de] bg-white text-[#57606a] dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300'
} }
}) })
const inputUsageLabel = computed(() => { const sourceIconClass = computed(() => {
const usage = props.messageMeta?.usage switch (props.messageMeta?.source) {
if (!usage || props.inversion) case 'semantic_match':
return '' return 'bg-emerald-50 text-emerald-600 shadow-[inset_0_0_0_1px_rgba(52,211,153,0.18)] dark:bg-emerald-950/35 dark:text-emerald-300'
case 'llm':
return t('chat.inputTokens', { count: usage.prompt_tokens }) return 'bg-slate-100 text-slate-500 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.18)] dark:bg-slate-800/70 dark:text-slate-300'
default:
return 'bg-white/90 text-[#52606d] shadow-[inset_0_0_0_1px_rgba(130,140,150,0.14)] dark:bg-neutral-800 dark:text-neutral-300'
}
}) })
const outputUsageLabel = computed(() => { const tokenStats = computed(() => {
const usage = props.messageMeta?.usage const usage = props.messageMeta?.usage
if (!usage || props.inversion) if (!usage || props.inversion)
return '' return []
return t('chat.outputTokens', { count: usage.completion_tokens }) return [
{
key: 'input',
label: t('chat.inputTokensShort'),
count: usage.prompt_tokens,
icon: 'ri:arrow-right-up-line',
},
{
key: 'output',
label: t('chat.outputTokensShort'),
count: usage.completion_tokens,
icon: 'ri:sparkling-line',
},
]
}) })
const options = computed(() => { const options = computed(() => {
@@ -138,30 +153,29 @@ function handleRegenerate() {
<div class="flex flex-col gap-2" :class="[inversion ? 'items-end' : 'items-start']"> <div class="flex flex-col gap-2" :class="[inversion ? 'items-end' : 'items-start']">
<span class="text-xs font-medium text-[#9aa4af] dark:text-neutral-500">{{ dateTime }}</span> <span class="text-xs font-medium text-[#9aa4af] dark:text-neutral-500">{{ dateTime }}</span>
<div <div
v-if="sourceLabel || inputUsageLabel || outputUsageLabel" v-if="sourceLabel || tokenStats.length"
class="flex flex-wrap items-center gap-2 rounded-2xl border border-[#e6edf3] bg-white/85 px-2.5 py-2 shadow-[0_10px_25px_rgba(15,23,42,0.06)] backdrop-blur-sm dark:border-neutral-800 dark:bg-[#14161a]/90" class="flex flex-wrap items-center gap-2"
> >
<div <div
v-if="sourceLabel" v-if="sourceLabel"
class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-semibold leading-none" class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-semibold leading-none shadow-sm"
:class="sourceClass" :class="sourceClass"
> >
<SvgIcon icon="ri:radar-line" /> <span class="inline-flex h-4 w-4 items-center justify-center rounded-full" :class="sourceIconClass">
<SvgIcon icon="ri:radar-line" />
</span>
<span>{{ sourceLabel }}</span> <span>{{ sourceLabel }}</span>
</div> </div>
<div <div
v-if="inputUsageLabel" v-for="item in tokenStats"
class="inline-flex items-center gap-1.5 rounded-full border border-violet-200 bg-violet-50 px-2.5 py-1 text-[11px] font-semibold leading-none text-violet-700 dark:border-violet-900/60 dark:bg-violet-950/40 dark:text-violet-300" :key="item.key"
class="inline-flex items-center gap-1.5 rounded-full border border-[#dfe8e3] bg-[#f7fbf8] px-2.5 py-1 text-[11px] font-medium leading-none text-[#5d7063] dark:border-[#2d3831] dark:bg-[#141916] dark:text-[#aebdb2]"
> >
<SvgIcon icon="ri:login-circle-line" /> <span class="inline-flex h-4 w-4 items-center justify-center rounded-full bg-white/90 text-[#4b9e5f] shadow-[inset_0_0_0_1px_rgba(75,158,95,0.12)] dark:bg-[#202622] dark:text-[#8ec79a]">
<span>{{ inputUsageLabel }}</span> <SvgIcon :icon="item.icon" />
</div> </span>
<div <span class="tracking-[0.02em]">{{ item.label }}</span>
v-if="outputUsageLabel" <span class="font-semibold tabular-nums text-[#355340] dark:text-[#dbe7de]">{{ item.count }}</span>
class="inline-flex items-center gap-1.5 rounded-full border border-fuchsia-200 bg-fuchsia-50 px-2.5 py-1 text-[11px] font-semibold leading-none text-fuchsia-700 dark:border-fuchsia-900/60 dark:bg-fuchsia-950/40 dark:text-fuchsia-300"
>
<SvgIcon icon="ri:logout-circle-r-line" />
<span>{{ outputUsageLabel }}</span>
</div> </div>
</div> </div>
</div> </div>

2
scripts/clean_faiss_and_kv.sh Executable file
View File

@@ -0,0 +1,2 @@
rm -rf ../faiss/indexes/global/global_qa.index
rm -rf ../kvstore/data/kvs_oplog.db

1
scripts/compiler.sh Executable file
View File

@@ -0,0 +1 @@
cd third_party/kvstore/ && make

View File

@@ -1,178 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
REDIS_PORT="${REDIS_PORT:-8888}"
REDIS_DB="${REDIS_DB:-0}"
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
PATTERNS=("ai_chat_service_*")
usage() {
cat <<'EOF'
Usage:
inspect_ai_redis.sh [options]
Options:
--host <host> Redis host, default: 127.0.0.1
--port <port> Redis port, default: 8888
--db <db> Redis db index, default: 0
--password <pwd> Redis password
--pattern <pattern> Key pattern, can be repeated. Default: ai_chat_service_*
--help Show this help
Environment:
REDIS_HOST
REDIS_PORT
REDIS_DB
REDIS_PASSWORD
Examples:
./scripts/inspect_ai_redis.sh
./scripts/inspect_ai_redis.sh --pattern 'ai_chat_service_*' --pattern 'foo_*'
REDIS_PORT=6379 ./scripts/inspect_ai_redis.sh --host 10.0.0.8
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
REDIS_HOST="$2"
shift 2
;;
--port)
REDIS_PORT="$2"
shift 2
;;
--db)
REDIS_DB="$2"
shift 2
;;
--password)
REDIS_PASSWORD="$2"
shift 2
;;
--pattern)
if [[ "${PATTERNS[*]}" == "ai_chat_service_*" && "${#PATTERNS[@]}" -eq 1 ]]; then
PATTERNS=()
fi
PATTERNS+=("$2")
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if ! command -v redis-cli >/dev/null 2>&1; then
echo "redis-cli not found in PATH" >&2
exit 1
fi
redis_cmd() {
if [[ -n "$REDIS_PASSWORD" ]]; then
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" -a "$REDIS_PASSWORD" --raw "$@"
else
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw "$@"
fi
}
print_string() {
local key="$1"
redis_cmd GET "$key"
}
print_hash() {
local key="$1"
redis_cmd HGETALL "$key" | awk 'NR % 2 == 1 { printf "%s: ", $0; next } { print $0 }'
}
print_list() {
local key="$1"
redis_cmd LRANGE "$key" 0 -1
}
print_set() {
local key="$1"
redis_cmd SMEMBERS "$key"
}
print_zset() {
local key="$1"
redis_cmd ZRANGE "$key" 0 -1 WITHSCORES | awk 'NR % 2 == 1 { printf "%s => ", $0; next } { print $0 }'
}
print_stream() {
local key="$1"
redis_cmd XRANGE "$key" - +
}
if ! redis_cmd PING >/dev/null 2>&1; then
echo "Failed to connect to Redis at ${REDIS_HOST}:${REDIS_PORT}, db=${REDIS_DB}" >&2
exit 1
fi
declare -A seen_keys=()
keys=()
for pattern in "${PATTERNS[@]}"; do
while IFS= read -r key; do
[[ -z "$key" ]] && continue
if [[ -z "${seen_keys[$key]:-}" ]]; then
seen_keys["$key"]=1
keys+=("$key")
fi
done < <(redis_cmd --scan --pattern "$pattern")
done
if [[ "${#keys[@]}" -eq 0 ]]; then
echo "No keys matched patterns: ${PATTERNS[*]}"
exit 0
fi
printf 'Redis: %s:%s db=%s\n' "$REDIS_HOST" "$REDIS_PORT" "$REDIS_DB"
printf 'Patterns: %s\n' "${PATTERNS[*]}"
printf 'Matched keys: %s\n\n' "${#keys[@]}"
for key in "${keys[@]}"; do
key_type="$(redis_cmd TYPE "$key" | tr -d '\r')"
ttl="$(redis_cmd TTL "$key" | tr -d '\r')"
echo "KEY: $key"
echo "TYPE: $key_type"
echo "TTL: $ttl"
echo "VALUE:"
case "$key_type" in
string)
print_string "$key"
;;
hash)
print_hash "$key"
;;
list)
print_list "$key"
;;
set)
print_set "$key"
;;
zset)
print_zset "$key"
;;
stream)
print_stream "$key"
;;
*)
echo "(unsupported type: $key_type)"
;;
esac
echo
done

4
scripts/run_kv.sh Executable file
View File

@@ -0,0 +1,4 @@
cd third_party/kvstore/
stdbuf -oL -eL nohup ./kvstore > /tmp/kvlog 2>&1 &
echo $! > /tmp/kvstore.pid
cat /tmp/kvstore.pid

1
third_party/kvstore vendored Submodule

Submodule third_party/kvstore added at 2f59d3ce27