feat: airi card (#98)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# @moeru-ai/ccc
|
||||
# @proj-airi/ccc
|
||||
|
||||
Create Character Card in a modular way.
|
||||
|
||||
|
||||
@@ -74,10 +74,48 @@ interface CardDescription {
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface CardExtra {
|
||||
/**
|
||||
* Character's personality traits and behavioral patterns
|
||||
* Used to define how the character should act and respond
|
||||
*/
|
||||
personality?: string
|
||||
|
||||
/**
|
||||
* Background context and setting for the character
|
||||
* Provides the environment and situation the character exists in
|
||||
*/
|
||||
scenario?: string
|
||||
|
||||
/**
|
||||
* Core system instructions for the character's behavior
|
||||
* Defines fundamental rules and context for the AI model
|
||||
*/
|
||||
systemPrompt?: string
|
||||
|
||||
/**
|
||||
* Instructions to process after chat history
|
||||
* Helps maintain character consistency across conversations
|
||||
*/
|
||||
postHistoryInstructions?: string
|
||||
|
||||
/**
|
||||
* Categorization labels for the character
|
||||
* Used for filtering and organization
|
||||
*/
|
||||
tags?: string[]
|
||||
|
||||
/**
|
||||
* Sample conversation snippets showing character interactions
|
||||
* Demonstrates expected conversation patterns
|
||||
*/
|
||||
messageExample?: Message[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* Moeru-AI Character Card
|
||||
*/
|
||||
export type Card = CardAdditional & CardCore & CardDescription & CardMeta
|
||||
export type Card = CardAdditional & CardCore & CardDescription & CardMeta & CardExtra
|
||||
|
||||
export type CardFn<T extends Record<string, unknown> = Record<string, unknown>> = (data: T) => Card
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ function createCardData(data: Card): CharacterCardV3['data'] {
|
||||
return {
|
||||
name: data.name,
|
||||
nickname: data.nickname,
|
||||
description: data.description ?? '', // TODO: improve description // FIXME: important
|
||||
personality: '', // TODO: add personality
|
||||
scenario: '', // TODO: add scenario
|
||||
description: data.description ?? '',
|
||||
personality: data.personality ?? '',
|
||||
scenario: data.scenario ?? '',
|
||||
first_mes: data.greetings?.[0] ?? '',
|
||||
alternate_greetings: data.greetings?.slice(1) ?? [],
|
||||
group_only_greetings: data.greetingsGroupOnly ?? [],
|
||||
@@ -33,10 +33,10 @@ function createCardData(data: Card): CharacterCardV3['data'] {
|
||||
creator: data.creator ?? '',
|
||||
creator_notes: data.notes ?? '',
|
||||
creator_notes_multilingual: data.notesMultilingual,
|
||||
system_prompt: '', // TODO: add system_prompt
|
||||
post_history_instructions: '', // TODO: add post_history_instructions
|
||||
system_prompt: data.systemPrompt ?? '',
|
||||
post_history_instructions: data.postHistoryInstructions ?? '',
|
||||
mes_example: formatMessageExample(data.messageExample),
|
||||
tags: [], // TODO: add tags
|
||||
tags: data.tags ?? [],
|
||||
extensions: createExtensions(data),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@proj-airi/ccc": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"radix-vue": "^1.9.17",
|
||||
"reka-ui": "^2.1.1",
|
||||
"unist-builder": "^4.0.0",
|
||||
"xast-util-to-xml": "^4.0.0",
|
||||
|
||||
@@ -12,7 +12,6 @@ interface ButtonProps {
|
||||
label?: string // Button text label
|
||||
disabled?: boolean // Disabled state
|
||||
loading?: boolean // Loading state
|
||||
onClick?: () => void // Click handler
|
||||
variant?: ButtonVariant // Button style variant
|
||||
size?: ButtonSize // Button size variant
|
||||
block?: boolean // Full width button
|
||||
@@ -59,7 +58,6 @@ const baseClasses = computed(() => [
|
||||
<button
|
||||
:disabled="isDisabled"
|
||||
:class="baseClasses"
|
||||
@click="onClick"
|
||||
>
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<div v-if="loading" class="i-lucide:loader-circle animate-spin" />
|
||||
|
||||
@@ -98,7 +98,7 @@ function toggleExpansion() {
|
||||
<!-- Description with ellipsis (limited to 2 lines) -->
|
||||
<TransitionVertical>
|
||||
<div
|
||||
v-if="!isExpanded"
|
||||
v-if="!isExpanded || !showExpandCollapse"
|
||||
class="line-clamp-2 cursor-pointer text-xs"
|
||||
:class="[
|
||||
modelValue === value
|
||||
|
||||
@@ -21,6 +21,7 @@ interface Props {
|
||||
customInputPlaceholder?: string
|
||||
expandButtonText?: string
|
||||
collapseButtonText?: string
|
||||
showMore?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -32,6 +33,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
customInputPlaceholder: 'Enter custom value',
|
||||
expandButtonText: 'Show more',
|
||||
collapseButtonText: 'Show less',
|
||||
showMore: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -120,7 +122,7 @@ function updateCustomValue(value: string) {
|
||||
:title="item.name"
|
||||
:description="item.description"
|
||||
:deprecated="item.deprecated"
|
||||
:show-expand-collapse="true"
|
||||
:show-expand-collapse="showMore"
|
||||
:expand-collapse-threshold="100"
|
||||
:show-custom-input="item.customizable"
|
||||
:custom-input-value="customValue"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
import type { AssistantMessage, Message } from '@xsai/shared-chat'
|
||||
import type { AssistantMessage, Message, SystemMessage } from '@xsai/shared-chat'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useLlmmarkerParser } from '../composables/llmmarkerParser'
|
||||
import SystemPromptV2 from '../constants/prompts/system-v2'
|
||||
import { useLLM } from '../stores/llm'
|
||||
import { asyncIteratorFromReadableStream } from '../utils/iterator'
|
||||
import { useAiriCardStore } from './modules'
|
||||
|
||||
export interface ErrorMessage {
|
||||
role: 'error'
|
||||
@@ -17,7 +16,7 @@ export interface ErrorMessage {
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const { stream } = useLLM()
|
||||
const { t } = useI18n()
|
||||
const { systemPrompt } = storeToRefs(useAiriCardStore())
|
||||
|
||||
const sending = ref(false)
|
||||
|
||||
@@ -63,10 +62,10 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
const messages = ref<Array<Message | ErrorMessage>>([
|
||||
SystemPromptV2(
|
||||
t('prompt.prefix'),
|
||||
t('prompt.suffix'),
|
||||
),
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt.value.replace(/\{\{user\}\}/g, 'user'),
|
||||
} satisfies SystemMessage,
|
||||
])
|
||||
|
||||
const streamingMessage = ref<AssistantMessage>({ role: 'assistant', content: '' })
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SystemPromptV2 from '../../constants/prompts/system-v2'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
import { useSpeechStore } from './speech'
|
||||
|
||||
export interface AiriExtension {
|
||||
modules: {
|
||||
consciousness: {
|
||||
model: string // Example: "gpt-4o"
|
||||
}
|
||||
|
||||
speech: {
|
||||
model: string // Example: "eleven_multilingual_v2"
|
||||
voice_id: string // Example: "alloy"
|
||||
|
||||
pitch?: number
|
||||
rate?: number
|
||||
ssml?: boolean
|
||||
language?: string
|
||||
}
|
||||
|
||||
vrm?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string // Example: "vrm/model.vrm"
|
||||
url?: string // Example: "https://example.com/vrm/model.vrm"
|
||||
}
|
||||
|
||||
live2d?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string // Example: "live2d/model.json"
|
||||
url?: string // Example: "https://example.com/live2d/model.json"
|
||||
}
|
||||
}
|
||||
|
||||
agents: {
|
||||
[key: string]: { // example: minecraft
|
||||
prompt: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AiriCard extends Card {
|
||||
extensions: {
|
||||
airi: AiriExtension
|
||||
} & Card['extensions']
|
||||
}
|
||||
|
||||
export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
const cards = useLocalStorage<Map<string, AiriCard>>('airi-cards', new Map())
|
||||
const activeCardId = useLocalStorage('airi-card-active-id', 'default')
|
||||
|
||||
const activeCard = computed(() => cards.value.get(activeCardId.value))
|
||||
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const speechStore = useSpeechStore()
|
||||
|
||||
const {
|
||||
activeModel: activeConsciousnessModel,
|
||||
} = storeToRefs(consciousnessStore)
|
||||
|
||||
const {
|
||||
activeSpeechVoiceId,
|
||||
activeSpeechModel,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const addCard = (card: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||
const newCardId = crypto.randomUUID()
|
||||
cards.value.set(newCardId, newAiriCard(card))
|
||||
return newCardId
|
||||
}
|
||||
|
||||
const removeCard = (id: string) => {
|
||||
cards.value.delete(id)
|
||||
}
|
||||
|
||||
const getCard = (id: string) => {
|
||||
return cards.value.get(id)
|
||||
}
|
||||
|
||||
function resolveAiriExtension(card: Card | ccv3.CharacterCardV3): AiriExtension {
|
||||
// Get existing extension if available
|
||||
const existingExtension = ('data' in card
|
||||
? card.data?.extensions?.airi
|
||||
: card.extensions?.airi) as AiriExtension
|
||||
|
||||
// Create default modules config
|
||||
const defaultModules = {
|
||||
consciousness: {
|
||||
model: activeConsciousnessModel.value,
|
||||
},
|
||||
speech: {
|
||||
model: activeSpeechModel.value,
|
||||
voice_id: activeSpeechVoiceId.value,
|
||||
},
|
||||
}
|
||||
|
||||
// Return default if no extension exists
|
||||
if (!existingExtension) {
|
||||
return {
|
||||
modules: defaultModules,
|
||||
agents: {},
|
||||
}
|
||||
}
|
||||
|
||||
// Merge existing extension with defaults
|
||||
return {
|
||||
modules: {
|
||||
consciousness: {
|
||||
model: existingExtension.modules?.consciousness?.model ?? defaultModules.consciousness.model,
|
||||
},
|
||||
speech: {
|
||||
model: existingExtension.modules?.speech?.model ?? defaultModules.speech.model,
|
||||
voice_id: existingExtension.modules?.speech?.voice_id ?? defaultModules.speech.voice_id,
|
||||
pitch: existingExtension.modules?.speech?.pitch,
|
||||
rate: existingExtension.modules?.speech?.rate,
|
||||
ssml: existingExtension.modules?.speech?.ssml,
|
||||
language: existingExtension.modules?.speech?.language,
|
||||
},
|
||||
vrm: existingExtension.modules?.vrm,
|
||||
live2d: existingExtension.modules?.live2d,
|
||||
},
|
||||
agents: existingExtension.agents ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
function newAiriCard(card: Card | ccv3.CharacterCardV3): AiriCard {
|
||||
// Handle ccv3 format if needed
|
||||
if ('data' in card) {
|
||||
const ccv3Card = card as ccv3.CharacterCardV3
|
||||
return {
|
||||
name: ccv3Card.data.name,
|
||||
version: ccv3Card.data.character_version ?? '1.0.0',
|
||||
description: ccv3Card.data.description ?? '',
|
||||
creator: ccv3Card.data.creator ?? '',
|
||||
notes: ccv3Card.data.creator_notes ?? '',
|
||||
notesMultilingual: ccv3Card.data.creator_notes_multilingual,
|
||||
personality: ccv3Card.data.personality ?? '',
|
||||
scenario: ccv3Card.data.scenario ?? '',
|
||||
greetings: [
|
||||
ccv3Card.data.first_mes,
|
||||
...(ccv3Card.data.alternate_greetings ?? []),
|
||||
],
|
||||
greetingsGroupOnly: ccv3Card.data.group_only_greetings ?? [],
|
||||
systemPrompt: ccv3Card.data.system_prompt ?? '',
|
||||
postHistoryInstructions: ccv3Card.data.post_history_instructions ?? '',
|
||||
messageExample: ccv3Card.data.mes_example
|
||||
? ccv3Card.data.mes_example
|
||||
.split('<START>\n')
|
||||
.filter(Boolean)
|
||||
.map(example => example.split('\n')
|
||||
.map((line) => {
|
||||
if (line.startsWith('{{char}}:') || line.startsWith('{{user}}:'))
|
||||
return line as `{{char}}: ${string}` | `{{user}}: ${string}`
|
||||
throw new Error(`Invalid message example format: ${line}`)
|
||||
}))
|
||||
: [],
|
||||
tags: ccv3Card.data.tags ?? [],
|
||||
extensions: {
|
||||
airi: resolveAiriExtension(ccv3Card),
|
||||
...ccv3Card.data.extensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...card,
|
||||
extensions: {
|
||||
airi: resolveAiriExtension(card),
|
||||
...card.extensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { t } = useI18n()
|
||||
cards.value.set('default', newAiriCard({
|
||||
name: 'ReLU',
|
||||
version: '1.0.0',
|
||||
// description: 'ReLU is a simple and effective activation function that is used in many neural networks.',
|
||||
description: SystemPromptV2(
|
||||
t('prompt.prefix'),
|
||||
t('prompt.suffix'),
|
||||
).content,
|
||||
}))
|
||||
})
|
||||
|
||||
watch(activeCard, (newCard: AiriCard | undefined) => {
|
||||
if (!newCard)
|
||||
return
|
||||
|
||||
// TODO: live2d, vrm
|
||||
// TODO: Minecraft Agent, etc
|
||||
const extension = resolveAiriExtension(newCard)
|
||||
if (!extension)
|
||||
return
|
||||
|
||||
activeConsciousnessModel.value = extension?.modules?.consciousness?.model
|
||||
activeSpeechModel.value = extension?.modules?.speech?.model
|
||||
activeSpeechVoiceId.value = extension?.modules?.speech?.voice_id
|
||||
})
|
||||
|
||||
return {
|
||||
cards,
|
||||
activeCard,
|
||||
activeCardId,
|
||||
addCard,
|
||||
removeCard,
|
||||
getCard,
|
||||
|
||||
currentModels: computed(() => {
|
||||
return {
|
||||
consciousness: {
|
||||
model: activeConsciousnessModel.value,
|
||||
},
|
||||
speech: {
|
||||
model: activeSpeechModel.value,
|
||||
voice_id: activeSpeechVoiceId.value,
|
||||
},
|
||||
} satisfies AiriExtension['modules']
|
||||
}),
|
||||
|
||||
systemPrompt: computed(() => {
|
||||
const card = activeCard.value
|
||||
if (!card)
|
||||
return ''
|
||||
|
||||
const components = [
|
||||
card.systemPrompt,
|
||||
card.description,
|
||||
card.personality,
|
||||
].filter(Boolean)
|
||||
|
||||
return components.join('\n')
|
||||
}),
|
||||
}
|
||||
})
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './airi-card'
|
||||
export * from './consciousness'
|
||||
export * from './speech'
|
||||
|
||||
Reference in New Issue
Block a user