Showing preview only (1,222K chars total). Download the full file or copy to clipboard to get everything.
Repository: lss233/kirara-ai
Branch: master
Commit: 8295a5deda0b
Files: 338
Total size: 1.1 MB
Directory structure:
gitextract_9czk0gzq/
├── .cursor/
│ └── rules/
│ └── create-workflow.mdc
├── .dockerignore
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── feature-request.md
│ ├── dependabot.yml
│ ├── quickstarts/
│ │ └── windows/
│ │ └── scripts/
│ │ └── 启动.cmd
│ └── workflows/
│ ├── docker-latest.yml
│ ├── docker-tag.yml
│ ├── pr_review.yml
│ ├── project_check.yml
│ ├── quickstart-windows.yml
│ ├── run-tests.yml
│ └── stale.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── README.md
├── alembic.ini
├── config.yaml.example
├── data/
│ ├── .gitkeep
│ ├── dispatch_rules/
│ │ └── rules.yaml
│ ├── media/
│ │ └── .gitignore
│ ├── memory/
│ │ └── .gitignore
│ ├── web/
│ │ └── .gitkeep
│ └── workflows/
│ ├── .gitkeep
│ └── chat/
│ ├── dsr_thinking.yaml
│ ├── memory_store.yaml
│ ├── normal_multimodal.yaml
│ └── talk_break.yaml
├── docker/
│ └── start.sh
├── kirara_ai/
│ ├── __init__.py
│ ├── __main__.py
│ ├── alembic/
│ │ ├── README
│ │ ├── env.py
│ │ ├── script.py.mako
│ │ └── versions/
│ │ └── 4a364dbb8dab_initial_migration.py
│ ├── config/
│ │ ├── __init__.py
│ │ ├── config_loader.py
│ │ └── global_config.py
│ ├── database/
│ │ ├── __init__.py
│ │ └── manager.py
│ ├── entry.py
│ ├── events/
│ │ ├── __init__.py
│ │ ├── application.py
│ │ ├── event_bus.py
│ │ ├── im.py
│ │ ├── listen.py
│ │ ├── llm.py
│ │ ├── plugin.py
│ │ ├── tracing/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── llm.py
│ │ └── workflow.py
│ ├── im/
│ │ ├── __init__.py
│ │ ├── adapter.py
│ │ ├── im_registry.py
│ │ ├── manager.py
│ │ ├── message.py
│ │ ├── profile.py
│ │ └── sender.py
│ ├── internal.py
│ ├── ioc/
│ │ ├── __init__.py
│ │ ├── container.py
│ │ └── inject.py
│ ├── llm/
│ │ ├── adapter.py
│ │ ├── format/
│ │ │ ├── __init__.py
│ │ │ ├── embedding.py
│ │ │ ├── message.py
│ │ │ ├── request.py
│ │ │ ├── rerank.py
│ │ │ ├── response.py
│ │ │ └── tool.py
│ │ ├── llm_manager.py
│ │ ├── llm_registry.py
│ │ └── model_types.py
│ ├── logger.py
│ ├── mcp_module/
│ │ ├── __init__.py
│ │ ├── manager.py
│ │ ├── models.py
│ │ └── server.py
│ ├── media/
│ │ ├── __init__.py
│ │ ├── carrier/
│ │ │ ├── __init__.py
│ │ │ ├── provider.py
│ │ │ ├── registry.py
│ │ │ └── service.py
│ │ ├── manager.py
│ │ ├── media_object.py
│ │ ├── metadata.py
│ │ ├── types/
│ │ │ ├── __init__.py
│ │ │ └── media_type.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── mime.py
│ ├── memory/
│ │ ├── composes/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── builtin_composes.py
│ │ │ ├── composer_strategy.py
│ │ │ ├── decomposer_strategy.py
│ │ │ └── xml_helper.py
│ │ ├── entry.py
│ │ ├── memory_manager.py
│ │ ├── persistences/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── codecs.py
│ │ │ ├── file_persistence.py
│ │ │ └── redis_persistence.py
│ │ ├── registry.py
│ │ └── scopes/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── builtin_scopes.py
│ ├── plugin_manager/
│ │ ├── models.py
│ │ ├── plugin.py
│ │ ├── plugin_event_bus.py
│ │ ├── plugin_loader.py
│ │ └── utils.py
│ ├── plugins/
│ │ ├── .gitkeep
│ │ ├── bundled_frpc/
│ │ │ ├── __init__.py
│ │ │ ├── frpc_manager.py
│ │ │ ├── models.py
│ │ │ └── routes.py
│ │ ├── im_http_legacy_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── setup.py
│ │ │ └── tests/
│ │ │ └── api_test.py
│ │ ├── im_qqbot_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── setup.py
│ │ │ └── utils.py
│ │ ├── im_telegram_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ └── setup.py
│ │ ├── im_wecom_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── delegates.py
│ │ │ └── setup.py
│ │ └── llm_preset_adapters/
│ │ ├── __init__.py
│ │ ├── alibabacloud_adapter.py
│ │ ├── claude_adapter.py
│ │ ├── deepseek_adapter.py
│ │ ├── gemini_adapter.py
│ │ ├── minimax_adapter.py
│ │ ├── mistral_adapter.py
│ │ ├── moonshot_adapter.py
│ │ ├── ollama_adapter.py
│ │ ├── openai_adapter.py
│ │ ├── openrouter_adapter.py
│ │ ├── setup.py
│ │ ├── siliconflow_adapter.py
│ │ ├── tencentcloud_adapter.py
│ │ ├── tests/
│ │ │ └── test_utils.py
│ │ ├── utils.py
│ │ ├── volcengine_adapter.py
│ │ └── voyage_adapter.py
│ ├── system/
│ │ ├── __init__.py
│ │ └── updater.py
│ ├── tracing/
│ │ ├── __init__.py
│ │ ├── core.py
│ │ ├── decorator.py
│ │ ├── llm_tracer.py
│ │ ├── manager.py
│ │ └── models.py
│ ├── web/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── block/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── diagnostics/
│ │ │ │ │ ├── base_diagnostic.py
│ │ │ │ │ ├── import_check.py
│ │ │ │ │ ├── jedi_syntax_check.py
│ │ │ │ │ ├── mandatory_function.py
│ │ │ │ │ └── pyflakes_check.py
│ │ │ │ ├── models.py
│ │ │ │ ├── python_lsp.py
│ │ │ │ └── routes.py
│ │ │ ├── dispatch/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── im/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── llm/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── mcp/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── media/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── plugin/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── system/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ ├── routes.py
│ │ │ │ └── utils.py
│ │ │ ├── tracing/
│ │ │ │ ├── __init__.py
│ │ │ │ └── routes.py
│ │ │ └── workflow/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ └── routes.py
│ │ ├── app.py
│ │ ├── auth/
│ │ │ ├── middleware.py
│ │ │ ├── models.py
│ │ │ ├── routes.py
│ │ │ ├── services.py
│ │ │ └── utils.py
│ │ └── utils.py
│ └── workflow/
│ ├── core/
│ │ ├── __init__.py
│ │ ├── block/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── input_output.py
│ │ │ ├── param.py
│ │ │ ├── registry.py
│ │ │ ├── schema.py
│ │ │ └── type_system.py
│ │ ├── dispatch/
│ │ │ ├── __init__.py
│ │ │ ├── dispatcher.py
│ │ │ ├── exceptions.py
│ │ │ ├── models/
│ │ │ │ └── dispatch_rules.py
│ │ │ ├── registry.py
│ │ │ └── rules/
│ │ │ ├── base.py
│ │ │ ├── message_rules.py
│ │ │ ├── sender_rules.py
│ │ │ └── system_rules.py
│ │ ├── execution/
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ └── executor.py
│ │ └── workflow/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── builder.py
│ │ └── registry.py
│ ├── implementations/
│ │ ├── __init__.py
│ │ ├── blocks/
│ │ │ ├── __init__.py
│ │ │ ├── game/
│ │ │ │ ├── dice.py
│ │ │ │ └── gacha.py
│ │ │ ├── im/
│ │ │ │ ├── basic.py
│ │ │ │ ├── messages.py
│ │ │ │ ├── states.py
│ │ │ │ └── user_profile.py
│ │ │ ├── llm/
│ │ │ │ ├── basic.py
│ │ │ │ ├── chat.py
│ │ │ │ └── image.py
│ │ │ ├── mcp/
│ │ │ │ ├── __init__.py
│ │ │ │ └── tool.py
│ │ │ ├── memory/
│ │ │ │ ├── chat_memory.py
│ │ │ │ └── clear_memory.py
│ │ │ ├── system/
│ │ │ │ ├── basic.py
│ │ │ │ └── help.py
│ │ │ ├── system_blocks.py
│ │ │ └── variables/
│ │ │ └── variable_blocks.py
│ │ ├── factories/
│ │ │ ├── __init__.py
│ │ │ ├── default_factory.py
│ │ │ ├── game_factory.py
│ │ │ └── system_factory.py
│ │ └── workflows/
│ │ ├── __init__.py
│ │ └── system_workflows.py
│ └── utils/
│ └── __init__.py
├── pyproject.toml
├── pytest.ini
└── tests/
├── __init__.py
├── llm_adapters/
│ ├── __init__.py
│ ├── conftest.py
│ ├── mock_app/
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── gemini.py
│ │ ├── models/
│ │ │ ├── gemini.py
│ │ │ └── openai.py
│ │ ├── ollama.py
│ │ ├── openai.py
│ │ └── voyage.py
│ ├── test_gemini_adapter.py
│ ├── test_ollama_adapter.py
│ ├── test_openai_adapter.py
│ └── test_voyage_adapter.py
├── memory/
│ ├── __init__.py
│ ├── test_composer_decomposer.py
│ ├── test_composer_strategy.py
│ ├── test_decomposer_strategy.py
│ ├── test_memory_manager.py
│ ├── test_persistence.py
│ └── test_scope.py
├── resources/
│ └── test_image.txt
├── system_blocks/
│ ├── __init__.py
│ ├── game/
│ │ ├── __init__.py
│ │ ├── test_dice.py
│ │ └── test_gacha.py
│ ├── im/
│ │ ├── __init__.py
│ │ ├── test_messages.py
│ │ └── test_states.py
│ ├── llm/
│ │ ├── __init__.py
│ │ ├── test_basic.py
│ │ ├── test_chat.py
│ │ └── test_image.py
│ ├── memory/
│ │ ├── __init__.py
│ │ ├── test_chat_memory.py
│ │ └── test_clear_memory.py
│ └── system/
│ ├── __init__.py
│ ├── test_basic.py
│ └── test_help.py
├── test_config_loader.py
├── test_game_blocks.py
├── test_mcp_server.py
├── test_media.py
├── test_media_element.py
├── test_system_blocks.py
├── test_workflow_builder.py
├── test_workflow_factories.py
├── tracing/
│ ├── __init__.py
│ ├── test_base.py
│ ├── test_core.py
│ ├── test_decorator.py
│ ├── test_llm_tracer.py
│ ├── test_manager.py
│ └── test_models.py
├── utils/
│ ├── auth_test_utils.py
│ └── test_block_registry.py
├── web/
│ ├── api/
│ │ ├── im/
│ │ │ └── test_im.py
│ │ ├── llm/
│ │ │ └── test_llm.py
│ │ ├── media/
│ │ │ └── test_media.py
│ │ ├── plugin/
│ │ │ └── test_plugin.py
│ │ ├── system/
│ │ │ └── test_system.py
│ │ └── workflow/
│ │ └── test_workflow.py
│ └── auth/
│ └── test_auth.py
└── workflow_executor/
├── test_block.py
├── test_executor.py
├── test_input_output.py
└── test_workflow_basic.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursor/rules/create-workflow.mdc
================================================
---
description: 创建 Workflow
globs:
---
You are an expert in Python, writing an AI application called chatgpt-mirai-qq-bot. it's a workflow based chatbot system.
Application
- Entrypoint: [main.py](mdc:main.py)
- IOC framework: [container.py](mdc:framework/ioc/container.py) [inject.py](mdc:framework/ioc/inject.py)
- Workflow system is consisted by a group of blocks that runs in workflow, workflow is run by executor.
- blocks: [base.py](mdc:framework/workflow/core/block/base.py) [registry.py](mdc:framework/workflow/core/block/registry.py)
- workflow: [base.py](mdc:framework/workflow/core/workflow/base.py) [registry.py](mdc:framework/workflow/core/workflow/registry.py)
- executor: [executor.py](mdc:framework/workflow/core/execution/executor.py)
- im adapter can choose which workflow to run in [dispatcher.py](mdc:framework/workflow/core/dispatch/dispatcher.py), rules is described by [rule.py](mdc:framework/workflow/core/dispatch/rule.py).
User defined rules located at folder `data/dispatch_rules`
- system/internal blocks implementation located at `framework/worflow/implementations`
- Memory system: [memory_manager.py](mdc:framework/memory/memory_manager.py).
Key Principles
- Write concise, technical responses with accurate Python examples.
- Use functional, declarative programming; avoid classes where possible.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., is_active, has_permission).
- Use lowercase with underscores for directories and files (e.g., routers/user_routes.py).
- Favor named exports for routes and utility functions.
- Use the Receive an Object, Return an Object (RORO) pattern.
Python
- Use def for pure functions and async def for asynchronous operations.
- Use type hints for all function signatures. Prefer Pydantic models over raw dictionaries for input validation.
- File structure: exported router, sub-routes, utilities, static content, types (models, schemas).
- Avoid unnecessary curly braces in conditional statements.
- For single-line statements in conditionals, omit curly braces.
- Use concise, one-line syntax for simple conditional statements (e.g., if condition: do_something()).
Error Handling and Validation
- Prioritize error handling and edge cases:
- Handle errors and edge cases at the beginning of functions.
- Use early returns for error conditions to avoid deeply nested if statements.
- Place the happy path last in the function for improved readability.
- Avoid unnecessary else statements; use the if-return pattern instead.
- Use guard clauses to handle preconditions and invalid states early.
- Implement proper error logging and user-friendly error messages.
- Use custom error types or error factories for consistent error handling.
Dependencies
- Pydantic v2
- Quart for HTTP Service
- rumel.yaml for YAML serialization
- asyncio for async programmig
Highlights on Workflow:
- IM Message get and send: [chat.py](mdc:framework/workflow/implementations/blocks/llm/chat.py)
- memory interaction: [chat_memory.py](mdc:framework/workflow/implementations/blocks/memory/chat_memory.py)
- LLM interaction: [chat.py](mdc:framework/workflow/implementations/blocks/llm/chat.py)
Key Conventions
1. Rely on App's dependency injection system for managing state and shared resources.
2. Respond in Chinese
================================================
FILE: .dockerignore
================================================
config.json
config.json.old
config.cfg
.chatgpt_cache.json
================================================
FILE: .editorconfig
================================================
# https://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
end_of_line = lf
[*.py]
max_line_length = 120
[LICENSE]
insert_final_newline = false
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug report
about: BUG 汇报
title: "[BUG] 请填写标题"
labels: bug
assignees: ''
---
**提交 issue 前,请先确认:**
- [x] 我已看过 **FAQ**,此问题不在列表中
- [ ] 我已看过其他 issue,他们不能解决我的问题
- [ ] 我认为这不是 Mirai 或者 OpenAI 的 BUG
**表现**
描述 BUG 的表现情况
**运行环境:**
- 操作系统:?
- Docker: ?
- 项目版本:?
**复现步骤**
描述你是如何触发这个 BUG 的
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**预期行为**
描述你认为正常情况下应该看见的情况
**截图**
相关日志、聊天记录的截图,没有可跳过
**其他内容**
此处填写其他内容,没有可跳过
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature request
about: 提交新功能建议
title: "[Feature] 请在此处填写标题"
labels: enhancement
assignees: ''
---
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/quickstarts/windows/scripts/启动.cmd
================================================
@REM ...
@ECHO OFF
@CHCP 65001
TITLE [Kirara AI] AI 系统正在启动...
SET PATH=%cd%\WPy64-31320\python;%cd%\ffmpeg\bin;%PATH%
IF NOT EXIST data\venv (
ECHO 虚拟环境不存在,正在创建...
python -m venv --system-site-packages data\venv
ECHO 虚拟环境创建完成
)
TITLE [Kirara AI] AI 系统正在运行...
ECHO 正在启动 Kirara AI...
call data\venv\Scripts\activate.bat
python -m kirara_ai
TITLE [Kirara AI] AI 系统已停止运行
ECHO 程序已停止运行。
PAUSE
================================================
FILE: .github/workflows/docker-latest.yml
================================================
name: Docker build latest
on:
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: qemu workaround
run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: 'linux/amd64,linux/arm64'
tags: lss233/kirara-agent-framework:latest
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/docker-tag.yml
================================================
name: Docker build with tags
on:
workflow_dispatch:
push:
tags:
- '**'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set output
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Check output
env:
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
run: |
echo $RELEASE_VERSION
echo ${{ steps.vars.outputs.tag }}
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: qemu workaround
run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: 'linux/amd64,linux/arm64'
tags: lss233/kirara-agent-framework:${{ steps.vars.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/pr_review.yml
================================================
name: PR Code Review
on:
pull_request_target:
branches: [ "master" ]
jobs:
mypy-review:
name: MyPy Type Check
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install project dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .
python -m pip install mypy types-requests types-setuptools
mypy --python-version 3.12 --ignore-missing-imports kirara_ai || true # run mypy to generate type dependencies
python -m mypy --install-types --non-interactive
- name: Get changed Python files
id: changed-files
run: |
BASE_SHA=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
CHANGED_FILES=$(git diff --name-only $BASE_SHA ${{ github.event.pull_request.head.sha }} | grep '\.py$' || echo "")
VALID_FILES=""
for file in $CHANGED_FILES; do
if [ -f "$file" ]; then
VALID_FILES="$VALID_FILES $file"
fi
done
echo "files=${VALID_FILES}" >> $GITHUB_OUTPUT
echo "Changed Python files: ${VALID_FILES}"
- name: Run mypy on changed files
id: run-mypy
run: |
CHANGED_FILES="${{ steps.changed-files.outputs.files }}"
if [[ -z "$CHANGED_FILES" ]]; then
echo "No Python files changed in this PR."
echo "has_changed_files=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changed_files=true" >> $GITHUB_OUTPUT
# 将输出保存到文本和JSON两种格式
mypy --python-version 3.12 --show-column-numbers --show-error-codes --ignore-missing-imports $CHANGED_FILES > mypy_output.txt || true
mypy --python-version 3.12 --show-column-numbers --show-error-codes --ignore-missing-imports $CHANGED_FILES --output json > mypy_output.json || true
continue-on-error: true
- name: Get PR diff information
id: get-diff
if: steps.run-mypy.outputs.has_changed_files == 'true'
run: |
# 获取被修改的文件和行号
BASE_SHA=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
git diff -U0 $BASE_SHA ${{ github.event.pull_request.head.sha }} > pr_diff.txt
# 解析diff,提取修改的行
python - <<'EOF'
import re
import json
changed_lines = {}
current_file = None
with open('pr_diff.txt', 'r') as f:
for line in f:
# 从diff头部获取文件名
file_match = re.match(r'^\+\+\+ b/(.+)', line)
if file_match:
current_file = file_match.group(1)
changed_lines[current_file] = []
continue
# 解析代码块修改,格式如:@@ -1,5 +1,9 @@
hunk_match = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line)
if hunk_match and current_file:
start_line = int(hunk_match.group(1))
if hunk_match.group(2):
count = int(hunk_match.group(2))
else:
count = 1
# 将这个块中所有增加或修改的行添加到列表
for i in range(count):
changed_lines[current_file].append(start_line + i)
# 将结果写入文件
with open('changed_lines.json', 'w') as f:
json.dump(changed_lines, f)
EOF
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Process mypy results
id: process-results
if: steps.run-mypy.outputs.has_changed_files == 'true'
run: |
python - <<'EOF'
#!/usr/bin/env python3
import json
import os
import re
# 读取diff信息,获取修改的行
try:
with open("changed_lines.json", "r") as f:
changed_lines = json.load(f)
except FileNotFoundError:
changed_lines = {}
# 读取文本输出
try:
with open("mypy_output.txt", "r") as f:
text_output = f.read()
except FileNotFoundError:
text_output = ""
# 读取JSON输出
mypy_results = []
try:
with open("mypy_output.json", "r") as f:
content = f.read().strip()
if content:
for line in content.splitlines():
try:
mypy_results.append(json.loads(line))
except json.JSONDecodeError:
continue
except FileNotFoundError:
pass
# 如果JSON解析失败,尝试从文本解析错误
if not mypy_results and text_output:
pattern = r"(.*?):(\d+):(\d+): (\w+): (.*)"
matches = re.findall(pattern, text_output)
for match in matches:
file_path, line, column, error_type, message = match
mypy_results.append({
"file": file_path,
"line": int(line),
"column": int(column),
"code": error_type,
"message": message
})
# 过滤掉不在PR变更文件中的错误,使用标准化路径和精确匹配来避免伪阳性错误
changed_files = os.environ.get('CHANGED_FILES', '').split()
if changed_files:
normalized_changed_files = [os.path.normpath(f) for f in changed_files]
mypy_results = [error for error in mypy_results if os.path.normpath(error.get('file', '')) in normalized_changed_files]
# 只保留diff中的错误
review_comments = []
diff_errors = [] # 存储在diff中的错误
for error in mypy_results:
file_path = error.get("file", "unknown")
line_num = error.get("line", 0)
col_num = error.get("column", 0)
message = error.get("message", "未知错误")
code = error.get("code", "unknown")
# 检查这一行是否在PR diff中被修改
is_changed_line = False
for changed_file in changed_lines:
if file_path.endswith(changed_file) and line_num in changed_lines[changed_file]:
is_changed_line = True
break
if is_changed_line:
# 如果是修改的行,创建行级评论
review_comments.append({
"path": file_path,
"line": line_num,
"body": f"**MyPy 类型错误**: {message} ({code})\n\n详细信息请参考 [mypy 文档](https://mypy.readthedocs.io/en/stable/error_code_list.html#{code.lower() if code != 'unknown' else 'error-codes'})。"
})
diff_errors.append(error)
# 创建摘要信息
if diff_errors:
status = "fail"
message = f"在 PR 修改的代码行中发现了 {len(diff_errors)} 个类型问题,需要修复。"
else:
status = "pass"
message = f"PR 修改的代码行通过了类型检查。"
summary = {
"status": status,
"diff_error_count": len(diff_errors),
"review_comment_count": len(review_comments),
"message": message
}
# 将评论数据保存为JSON文件
with open("mypy_review_comments.json", "w") as f:
json.dump(review_comments, f)
# 将摘要保存为JSON文件
with open("mypy_summary.json", "w") as f:
json.dump(summary, f)
# 写入输出
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"result={status}\n")
f.write(f"diff_error_count={len(diff_errors)}\n")
f.write(f"review_comment_count={len(review_comments)}\n")
EOF
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.files }}
- name: Post line-level PR review comments
if: steps.process-results.outputs.diff_error_count != '0'
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
// 读取评论数据
const reviewComments = JSON.parse(fs.readFileSync('mypy_review_comments.json', 'utf8'));
const summary = JSON.parse(fs.readFileSync('mypy_summary.json', 'utf8'));
// 创建PR审查
const review = await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: `## MyPy 类型检查结果 ❌\n\n${summary.message}\n\n已对修改的代码行创建了 ${reviewComments.length} 个行级评论。`,
event: 'COMMENT',
comments: reviewComments
});
// 添加失败标签
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['🔴 类型检查:失败']
});
console.log(`Created review with ${reviewComments.length} comments`);
- name: Post success comment
if: steps.process-results.outputs.result == 'pass'
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('mypy_summary.json', 'utf8'));
// 查找之前的评论
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c => {
return c.user.type === 'Bot' &&
(c.body.includes('MyPy 类型检查通过') || c.body.includes('MyPy 类型检查结果'));
});
const comment = `## MyPy 类型检查通过 ✅\n\n${summary.message}`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
// 移除失败标签(如果存在)并添加成功标签
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: '🔴 类型检查:失败'
});
} catch (error) {
// 标签可能不存在,忽略错误
}
// 添加成功标签
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['✅ 类型检查:通过']
});
- name: Post notification if no Python files changed
if: steps.run-mypy.outputs.has_changed_files == 'false'
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment => {
return comment.user.type === 'Bot' &&
(comment.body.includes('MyPy 类型检查通过') || comment.body.includes('MyPy 类型检查结果'));
});
const comment = "## MyPy 类型检查\n\nPR 中没有修改任何 Python 文件,跳过类型检查。";
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
// 移除类型检查相关标签(如果存在)
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: '🔴 类型检查:失败'
});
} catch (error) {
// 标签可能不存在,忽略错误
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: '✅ 类型检查:通过'
});
} catch (error) {
// 标签可能不存在,忽略错误
}
- name: Fail if type issues found in diff
if: steps.process-results.outputs.diff_error_count != '0'
run: exit 1
================================================
FILE: .github/workflows/project_check.yml
================================================
name: Project Check
on:
push:
branches: [ "master" ]
merge_group:
branches: [ "master" ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
issues: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install project dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .
python -m pip install mypy types-requests types-setuptools
mypy --python-version 3.10 --ignore-missing-imports kirara_ai || true # run mypy to generate type dependencies
python -m mypy --install-types --non-interactive
- name: Run mypy
run: |
mypy --python-version 3.10 --show-column-numbers --show-error-codes --ignore-missing-imports kirara_ai --output json > mypy_output.json
continue-on-error: true
- name: Create mypy issue content
if: always()
run: |
cat > create_mypy_issue.py << 'EOF'
#!/usr/bin/env python3
import json
import os
import sys
from datetime import datetime
from collections import defaultdict
# 读取 mypy JSON 输出
try:
with open("mypy_output.json", "r") as f:
content = f.read()
if content.strip():
mypy_results = [json.loads(line) for line in content.splitlines() if line.strip()]
else:
print("mypy_output.json 文件为空")
mypy_results = []
except FileNotFoundError:
print("警告:mypy_output.json 文件不存在。创建空结果列表。")
mypy_results = []
except json.JSONDecodeError as e:
print(f"解析 JSON 时出错: {e}")
with open("mypy_output.json", "r") as f:
print(f"文件内容: {f.read()[:1000]}")
mypy_results = []
# 如果没有结果,则退出
if not mypy_results:
print("没有发现类型错误,不创建 issue")
sys.exit(0)
# 获取仓库信息
repo = os.environ.get("GITHUB_REPOSITORY", "")
run_id = os.environ.get("GITHUB_RUN_ID", "")
sha = os.environ.get("GITHUB_SHA", "")[:7]
# 获取分支信息
ref_name = os.environ.get("GITHUB_REF_NAME", "")
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
if event_name == "merge_group":
branch_info = f"合并到 {ref_name} 分支"
else:
branch_info = f"{ref_name} 分支"
# 格式化当前时间
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 按文件分组错误
errors_by_file = defaultdict(list)
# 按错误类型分组
errors_by_type = defaultdict(int)
for result in mypy_results:
file_path = result.get("file", "unknown")
error_code = result.get("code", "未知")
errors_by_file[file_path].append(result)
errors_by_type[error_code] += 1
# 创建 issue 标题
issue_title = f"对{branch_info}的类型检查发现了 {len(mypy_results)} 个问题 ({sha})"
# 直接设置环境变量而不是写入文件
with open(os.environ['GITHUB_ENV'], 'a') as f:
f.write(f"ISSUE_TITLE={issue_title}\n")
# 创建 issue 内容
issue_body = f"""## mypy 类型检查报告
**时间**: {now}
**分支**: {branch_info}
**Commit**: {sha}
**工作流**: [查看运行详情](https://github.com/{repo}/actions/runs/{run_id})
mypy 共发现 {len(mypy_results)} 个类型问题:
"""
# 添加错误类型统计
issue_body += "### 错误类型统计\n\n"
issue_body += "| 错误代码 | 出现次数 | 占比 |\n"
issue_body += "| -------- | -------- | ---- |\n"
for error_code, count in sorted(errors_by_type.items(), key=lambda x: x[1], reverse=True):
percentage = (count / len(mypy_results)) * 100
issue_body += f"| `{error_code}` | {count} | {percentage:.1f}% |\n"
# 添加每个文件的问题摘要
issue_body += "\n### 问题摘要\n\n"
issue_body += "| 文件 | 问题数量 | 详情 |\n"
issue_body += "| ---- | -------- | ---- |\n"
for file_path, errors in sorted(errors_by_file.items(), key=lambda x: len(x[1]), reverse=True):
file_short = file_path.split("/")[-1]
issue_body += f"| `{file_path}` | {len(errors)} | [查看详情](#file-{file_short.replace('.', '-')}) |\n"
issue_body += "\n### 详细问题\n\n"
# 添加每个文件的详细问题
for file_path, errors in sorted(errors_by_file.items(), key=lambda x: x[0]):
file_short = file_path.split("/")[-1]
issue_body += f"<a id=\"file-{file_short.replace('.', '-')}\"></a>\n"
issue_body += f"#### {file_path}\n\n"
issue_body += "| 行号 | 列号 | 错误代码 | 错误消息 |\n"
issue_body += "| ---- | ---- | -------- | -------- |\n"
# 按行号排序错误
for error in sorted(errors, key=lambda x: (x.get("line", 0), x.get("column", 0))):
line = error.get("line", "-")
column = error.get("column", "-")
error_code = error.get("code", "未知")
message = error.get("message", "").replace("|", "\\|") # 转义管道符号,避免破坏表格
issue_body += f"| {line} | {column} | `{error_code}` | {message} |\n"
issue_body += "\n"
# 将内容写入文件,供 GitHub Action 使用
with open("issue_body.md", "w", encoding="utf-8") as f:
f.write(issue_body)
print(f"成功创建 mypy 问题报告,共 {len(mypy_results)} 个问题")
EOF
python create_mypy_issue.py
- name: Create GitHub Issue
if: always()
uses: peter-evans/create-issue-from-file@v4
with:
title: ${{ env.ISSUE_TITLE }}
content-filepath: ./issue_body.md
labels: |
type-check
automated-report
bug
================================================
FILE: .github/workflows/quickstart-windows.yml
================================================
name: Windows Quickstart
on:
workflow_dispatch:
push:
branches: ['master']
tags: ['**']
pull_request:
branches: ['master']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.13.2.0"
WINPYTHON_URL: "https://github.com/winpython/winpython/releases/download/13.1.202502222final/Winpython64-3.13.2.0dot.zip"
DIST_DIR: "C:/dist"
BUILD_DIR: "C:/build"
PACKAGE_NAME: "quickstart-windows-kirara-ai-amd64"
jobs:
build:
name: Windows Quickstart
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python for building
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Build wheel package
run: |
python -m pip install build
python -m build
# 获取生成的wheel文件名
$WheelFile = Get-ChildItem -Path "dist" -Filter "*.whl" | Select-Object -First 1 -ExpandProperty Name
echo "WHEEL_FILE=$WheelFile" >> $env:GITHUB_ENV
- name: Prepare distribution environment
run: |
# 创建必要的目录
mkdir ${{ env.DIST_DIR }}
mkdir ${{ env.BUILD_DIR }}
# 下载 WinPython
Invoke-WebRequest -Uri "${{ env.WINPYTHON_URL }}" -OutFile "${{ env.BUILD_DIR }}/winpython.zip"
Expand-Archive "${{ env.BUILD_DIR }}/winpython.zip" -DestinationPath "${{ env.DIST_DIR }}"
- name: Install project and dependencies
run: |
cd ${{ env.DIST_DIR }}
./WPy64-31320/python/python.exe -m pip install "${{ github.workspace }}/dist/${{ env.WHEEL_FILE }}"
./WPy64-31320/python/python.exe -m pip install --upgrade pip
- name: Download and setup FFmpeg
run: |
Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.0.2-essentials_build.7z" -OutFile "${{ env.BUILD_DIR }}/ffmpeg.7z"
7z x "${{ env.BUILD_DIR }}/ffmpeg.7z" -o"${{ env.DIST_DIR }}/ffmpeg"
mv "${{ env.DIST_DIR }}/ffmpeg/ffmpeg-7.0.2-essentials_build" "${{ env.DIST_DIR }}/ffmpeg/bin"
- name: Download VC++ Runtime
run: |
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile "${{ env.DIST_DIR }}/【语音功能依赖】vc_redist.x64.exe"
- name: Setup Web UI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 下载 Web UI 压缩包到临时目录
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/DarkSkyTeam/chatgpt-for-bot-webui/releases" -Headers @{Authorization = "Bearer $env:GH_TOKEN"}
$web_ui_url = $release[0].assets[0].browser_download_url
$zip_file = "${{ env.BUILD_DIR }}/webui.zip"
Invoke-WebRequest -Uri $web_ui_url -OutFile $zip_file
# 解压到临时目录
$temp_dir = "${{ env.BUILD_DIR }}/webui_temp"
mkdir $temp_dir
Expand-Archive -Path $zip_file -DestinationPath $temp_dir
New-Item -ItemType Directory -Force -Path "${{ env.DIST_DIR }}/web"
# 移动 dist 文件夹到目标位置
Copy-Item -Path "$temp_dir/dist/*" -Destination "${{ env.DIST_DIR }}/web" -Force -Recurse
- name: Copy startup scripts
run: |
Copy-Item ".github/quickstarts/windows/scripts/*" -Destination "${{ env.DIST_DIR }}/" -Recurse
# 拷贝 data 文件夹
Copy-Item -Path "${{ github.workspace }}/data" -Destination "${{ env.DIST_DIR }}/" -Recurse
- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}
path: ${{ env.DIST_DIR }}
- name: Create release archive
if: startsWith(github.ref, 'refs/tags/')
run: |
Compress-Archive -Path "${{ env.DIST_DIR }}/*" -DestinationPath "${{ env.BUILD_DIR }}/${{ env.PACKAGE_NAME }}.zip"
- name: Upload release archive
if: startsWith(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.BUILD_DIR }}/${{ env.PACKAGE_NAME }}.zip
asset_name: Windows-quickstart-kirara-ai-${{ github.ref_name }}.zip
tag: ${{ github.ref_name }}
overwrite: false
body: "Windows x64 用户的快速启动包"
================================================
FILE: .github/workflows/run-tests.yml
================================================
name: Run Tests
on:
workflow_dispatch:
push:
branches:
- '**'
pull_request:
branches:
- master
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Set up Docker
if: matrix.os == 'ubuntu-latest'
uses: docker/setup-docker-action@v4
- name: Build Docker image
if: matrix.os == 'ubuntu-latest'
run: |
docker build -t test-image .
- name: Run tests in Docker
if: matrix.os == 'ubuntu-latest'
run: |
docker run -v $(pwd):/app test-image sh -c "python -m pip install pytest coverage pytest-cov && python -m pytest /app/tests -v --cov=kirara_ai --cov-report=xml:/app/coverage.xml --cov-report=term-missing --junitxml=/app/junit.xml -o junit_family=legacy"
- name: Upload test results to Codecov
if: matrix.os == 'ubuntu-latest'
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
if: matrix.os == 'ubuntu-latest'
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Set up Python 3.13
if: matrix.os == 'windows-latest'
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Run tests on Windows
if: matrix.os == 'windows-latest'
run: |
set PYTHONIOENCODING=utf-8
set PYTHONLEGACYWINDOWSSTDIO=utf-8
python -m pip install -e .
python -m pip install pytest
chcp 65001
python -m pytest ./tests -vs
================================================
FILE: .github/workflows/stale.yml
================================================
name: 处理不活跃的 Issue 和 PR
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *' # 每天午夜运行
permissions:
contents: write # only for delete-branch option
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
with:
# 基本配置
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60 # 60天不活跃标记为 stale
days-before-close: 14 # 标记为 stale 后14天关闭
# 友好的提示信息
stale-issue-message: >
👋 您好!这个 issue 已经 60 天没有活动了。
为了保持我们的 issue 列表整洁,我们会标记长时间不活跃的 issue。
如果您认为这个 issue 仍然重要且有效,请留下评论或移除 "stale" 标签,
否则它将在 14 天后自动关闭。
感谢您的理解和贡献!
stale-pr-message: >
👋 您好!这个 PR 已经 60 天没有活动了。
为了保持我们的 PR 列表整洁,我们会标记长时间不活跃的 PR。
如果您仍在处理这个 PR,请留下评论或移除 "stale" 标签,
否则它将在 14 天后自动关闭。
如果您需要帮助完成这个 PR,请告诉我们!
感谢您的贡献!
close-issue-message: >
🙏 由于长时间没有活动,我们暂时关闭了这个 issue。
如果您认为这个问题仍然存在,请随时重新打开或创建新的 issue。
谢谢!
close-pr-message: >
🙏 由于长时间没有活动,我们暂时关闭了这个 PR。
如果您想继续这项工作,请随时重新打开或创建新的 PR。
感谢您的贡献!
# 排除某些标签的 issue/PR
exempt-issue-labels: 'planned,documentation,long-term-task'
exempt-pr-labels: 'WIP,waiting-for-review,long-term-task'
# 只处理某些标签的 issue/PR(可选)
# only-labels: ''
# 其他选项
operations-per-run: 100 # 每次运行处理的最大数量
remove-stale-when-updated: true # 当更新时移除 stale 标签
ascending: true # 从最老的开始处理
================================================
FILE: .gitignore
================================================
config.json
config.cfg
__pycache__/
python3.11/
.idea/
data/*.json
.chatgpt_cache.json
Dockerfile.dev
**/.DS_Store
venv/
.vscode/
config.yaml
config.yaml.bak
data/config.yaml
data/config.yaml.bak
logs/
**/password.hash
dist/
build/
*.egg-info/
.coverage
/web/
botpy.log*
data/frpc/
data/db
.venv/
uv.lock
**/mypy_cache/
**/test-workflow-new.yaml
**/test_password.hash
================================================
FILE: .pre-commit-config.yaml
================================================
repos:
- repo: https://github.com/PyCQA/isort
rev: 6.0.0
hooks:
- id: isort
name: isort (python3)
language_version: python3
args: ["--atomic"]
- repo: https://github.com/myint/autoflake
rev: v2.3.0
hooks:
- id: autoflake
args:
[
"--remove-all-unused-imports",
"--in-place",
"--recursive",
]
================================================
FILE: .pylintrc
================================================
[MASTER]
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0
[MESSAGES CONTROL]
# Disable the message, report, category or checker with the given id(s).
disable=all
# Enable the message, report, category or checker with the given id(s).
enable=c-extension-no-member,
bad-indentation,
bare-except,
broad-except,
dangerous-default-value,
function-redefined,
len-as-condition,
line-too-long,
misplaced-future,
missing-final-newline,
mixed-line-endings,
multiple-imports,
multiple-statements,
singleton-comparison,
trailing-comma-tuple,
trailing-newlines,
trailing-whitespace,
unexpected-line-ending-format,
unused-import,
unused-variable,
wildcard-import,
wrong-import-order
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=LF
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module.
max-module-lines=2000
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
================================================
FILE: Dockerfile
================================================
# 第一阶段:构建wheel包
FROM python:3.11-slim AS builder
WORKDIR /build
COPY . .
RUN python -m pip install build && \
python -m build
# 第二阶段:运行环境
FROM python:3.11-slim-bullseye
ENV DEBIAN_FRONTEND=noninteractive
# 复制字体文件
COPY ./data/fonts/sarasa-mono-sc-regular.ttf /usr/share/fonts/
# 安装系统依赖
RUN apt-get -yqq update && \
apt-get -yqq install --no-install-recommends \
wkhtmltopdf \
ffmpeg \
curl \
jq \
libmagic1 \
unzip && \
apt-get -yq clean && \
apt-get -yq purge --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \
rm -rf /var/lib/apt/lists/*
# 创建应用目录
WORKDIR /app
# 复制第一阶段构建的wheel包并安装
COPY --from=builder /build/dist/*.whl /app/
# 下载Web UI并安装依赖
RUN PACKAGE_INFO=$(curl -s https://registry.npmjs.org/kirara-ai-webui) && \
LATEST_VERSION=$(printf %s $PACKAGE_INFO | jq -r '.["dist-tags"].latest') && \
TARBALL_URL=$(printf %s $PACKAGE_INFO | jq -r --arg VERSION "$LATEST_VERSION" '.versions[$VERSION].dist.tarball') && \
curl -L -o webui.tgz "$TARBALL_URL" && \
mkdir -p /tmp/webui && \
tar -xzf webui.tgz -C /tmp/webui && \
mkdir -p /app/web && \
cp -r /tmp/webui/package/dist/* /app/web/ && \
rm -rf /tmp/webui webui.tgz && \
pip install --no-cache-dir *.whl && \
pip cache purge && \
rm *.whl
# 移除不再需要的包
RUN apt-get -yqq remove --purge curl jq unzip
# 复制应用代码
COPY ./docker/start.sh /app/docker/
COPY ./data /tmp/data
EXPOSE 8080
CMD ["/bin/bash", "/app/docker/start.sh"]
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================
FILE: MANIFEST.in
================================================
recursive-include kirara_ai/plugins/im_http_legacy_adapter/assets *
recursive-include kirara_ai/plugins/im_qqbot_adapter/assets *
recursive-include kirara_ai/plugins/im_telegram_adapter/assets *
recursive-include kirara_ai/plugins/im_wecom_adapter/assets *
recursive-include kirara_ai/alembic *
================================================
FILE: README.md
================================================
<p align="center">
<h2 align="center">Kirara AI</h2>
<p align="center">
一款支持主流大语言模型、主流聊天平台的聊天的机器人!
<br/>
<br/>
<a href="https://kirara-docs.app.lss233.com/"><strong>» 查看项目手册 »</strong></a>
<br/>
</p>
</p>
<p align="center">
<a href="https://github.com/lss233/kirara-ai/stargazers"><img src="https://img.shields.io/github/stars/lss233/kirara-ai?color=F8B195&logo=github&style=for-the-badge" alt="Github stars"></a>
<a href="https://pypi.org/project/kirara-ai/"><img src="https://img.shields.io/pypi/v/kirara-ai?color=F67280&logo=pypi&logoColor=white&style=for-the-badge" alt="PyPI"></a>
<a href="./LICENSE"><img src="https://img.shields.io/github/license/lss233/kirara-ai?&color=C06C84&style=for-the-badge" alt="License"></a>
</p>
<p align="center">
<a href="https://github.com/lss233/kirara-ai/actions/workflows/docker-latest.yml"><img src="https://img.shields.io/github/actions/workflow/status/lss233/kirara-ai/docker-latest.yml?color=6C5B7B&logo=docker&logoColor=white&style=for-the-badge" alt="Docker build latest"></a>
<a href="https://hub.docker.com/r/lss233/kirara-ai/"><img src="https://img.shields.io/docker/pulls/lss233/kirara-agent-framework?color=355C7D&logo=docker&logoColor=white&style=for-the-badge" alt="Docker Pulls"></a>
<a href="https://codecov.io/gh/lss233/kirara-ai"><img alt="Codecov" src="https://img.shields.io/codecov/c/gh/lss233/kirara-ai?color=A8E6CE&logo=codecov&logoColor=white&style=for-the-badge"></a>
<img alt="Mypy checked" src="https://img.shields.io/badge/Mypy-checked-DCEDC2?style=for-the-badge&logo=python&logoColor=white">
</p>
***

***
## 🌟 社区交流
加入我们的社区,获取最新项目动态、视频教程、问题答疑和技术交流!
* QQ 交流群:
* [二群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=S1R4eIlODtyKZsEKfWxb2-nOIHELbeJY&authKey=kAftCAALE8OJgwQnArrD6zPtncCAaY456QgUXT3l2OMJ57NwRXRkhv4KL7DzOLzs&noverify=0&group_code=373254418)(已满)
* [三群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=urlhCH8y7Ro2S-iXt63X4s5eILUny4Iw&authKey=ejiwoNa4Yez6IMLyf2vj%2FeRiC1frdFrNNekbRfaPnSQbcD7bgebo5y5A7rPaRKBq&noverify=0&group_code=533109074)(已满)
* [四群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Ibiu6EmXof30Fa7MJ5j8nJFwaUGTf5bM&authKey=YKx5a%2BK5qnWkk5VlsxxDfYl0nCrKSekQm%2FoLQVqr%2FcO%2FQY2S6N24XdI23XugBrF0&noverify=0&group_code=799737883)(已满)
* [五群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=lDkVPDAeiz6M-ig9cdS9tqhSH6_topox&authKey=B%2FRPYVUjk3dYPw5D4o6C2TpqeoKTG0nXEiKDCG%2Bh4JYY2RPqDQGt37SGl32j0hHw&noverify=0&group_code=805081636)
* [六群](https://qm.qq.com/q/UpvYm3jccg)
> **提问前请先查看**: 加入群组前,请先查看[项目问题列表](https://github.com/lss233/kirara-ai/issues),看是否能解决你的问题。
>
> 如需提问,请准备好问题描述、**完整日志**和相关配置文件,以便我们更好地帮助你。
> 进群请备注:GitHub
* [机器人调试群](https://jq.qq.com/?_wv=1027&k=TBX8Saq7) - 这里有多个 QQ 机器人供体验,不解答技术问题。
* [开发者交流群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=lisyXibhUj93DgIZptQu3VZ4ka3F5-rW&authKey=PBCzRQX4Zei%2BB6n5Tdyp9p5bqcF0tLBlfGANT4dSSKQIFYR66WwaZSMEDahWo%2FzZ&noverify=0&group_code=701933732) - 欢迎参与 Kirara AI 及生态开发 / 对大模型应用有兴趣的开发者加入,一起交流学习。
## 📷 功能展示
|  |  |  |
|:-------------------------------:|:-------------------------------:|:-------------------------------:|
|  |  |  |
## 🧭 WebUI
<div align="center">
<h3 align="center">模型管理</h3>

<h3 align="center">工作流</h3>

<h3 align="center">插件市场</h3>

</div>
## ⚡ 核心特性
* [x] 图片发送
* [x] 关键词触发回复
* [x] 多账号支持
* [x] 人格设定
* [x] 支持 QQ、Telegram、Discord、微信
* [x] 可作为 HTTP 服务端提供 Web API
* [x] 支持 OpenAI、DeepSeek、Claude、Gemini、Qwen、Mistral、豆包、Minimax、Kimi、Mistral 等主流大模型
* [x] 支持插件机制
* [x] 支持条件触发
* [x] 支持管理员指令
* [x] 支持 Stable Diffusion、Flux、Midjourney 等绘图模型
* [x] 支持语音回复
* [x] 支持多轮对话
* [x] 支持跨平台消息发送
* [x] 支持自定义工作流
* [x] 支持 Web 管理后台
* [x] 内置 Frpc 内网穿透
# **🤖 聊天平台**
我们支持多种聊天平台。
| 平台 | 群聊回复 | 私聊回复 | 条件触发 | 管理员指令 | 绘图 | 语音回复 |
|----------|------|------|------|-------|-----|------|
| Telegram | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| QQ 机器人 | 支持 | 支持 | 支持 | 支持 | 支持 | 平台不支持 |
| Discord | 重构中 | 重构中 | 重构中 | 重构中 | 重构中 | 重构中 |
| 飞书机器人 | 重构中 | 重构中 | 重构中 | 重构中 | 重构中 | 重构中 |
| 企业微信应用 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
| 微信公众号 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
| OneBot | 插件支持 | 插件支持 | 插件支持 | 插件支持 | 插件支持 | 插件支持 |
## 🐎 命令
**你可以在 WebUI 的调度规则中自定义所有命令。**
## 🔧 搭建
请移步至 [快速开始](https://kirara-docs.app.lss233.com/guide/getting-started.html)
## 🕸 HTTP API
<details>
<summary>HTTP API 可用于接入其他平台。</summary>
在聊天平台管理中启动 http-legacy 适配器后,将提供以下接口:
**POST** `/v1/chat`
**请求参数**
|参数名|必选|类型|说明|
|:---|:---|:---|:---|
|session_id| 是 | String |会话ID,默认:`friend-default_session`|
|username| 是 | String |用户名,默认:`某人`|
|message| 是 | String |消息,不能为空|
**请求示例**
```json
{
"session_id": "friend-123456",
"username": "testuser",
"message": "ping"
}
```
**响应格式**
|参数名|类型|说明|
|:---|:---|:---|
|result| String |SUCESS,DONE,FAILED|
|message| String[] |文本返回,支持多段返回|
|voice| String[] |音频返回,支持多个音频的base64编码;参考:data:audio/mpeg;base64,...|
|image| String[] |图片返回,支持多个图片的base64编码;参考:data:image/png;base64,...|
**响应示例**
```json
{
"result": "DONE",
"message": ["pong!"],
"voice": [],
"image": []
}
```
**POST** `/v2/chat`
**请求参数**
|参数名|必选|类型|说明|
|:---|:---|:---|:---|
|session_id| 是 | String |会话ID,默认:`friend-default_session`|
|username| 是 | String |用户名,默认:`某人`|
|message| 是 | String |消息,不能为空|
**请求示例**
```json
{
"session_id": "friend-123456",
"username": "testuser",
"message": "ping"
}
```
**响应格式**
字符串:request_id
**响应示例**
```
1681525479905
```
**GET** `/v2/chat/response`
**请求参数**
|参数名|必选|类型|说明|
|:---|:---|:---|:---|
|request_id| 是 | String |请求id,/v2/chat返回的值|
**请求示例**
```
/v2/chat/response?request_id=1681525479905
```
**响应格式**
|参数名|类型|说明|
|:---|:---|:---|
|result| String |SUCESS,DONE,FAILED|
|message| String[] |文本返回,支持多段返回|
|voice| String[] |音频返回,支持多个音频的base64编码;参考:data:audio/mpeg;base64,...|
|image| String[] |图片返回,支持多个图片的base64编码;参考:data:image/png;base64,...|
* 每次请求返回增量并清空。DONE、FAILED之后没有更多返回。
**响应示例**
```json
{
"result": "DONE",
"message": ["pong!"],
"voice": ["data:audio/mpeg;base64,..."],
"image": ["data:image/png;base64,...", "data:image/png;base64,..."]
}
```
</details>
## 🦊 加载预设
如果你想让机器人自动带上某种聊天风格,可以使用预设功能。
我们自带了 `猫娘` 和 `正常` 两种预设,你可以在 `presets` 文件夹下了解预设的写法。
使用 `加载预设 猫娘` 来加载猫娘预设。
下面是一些预设的小视频,你可以看看效果:
* MOSS: https://www.bilibili.com/video/av352047018
* 丁真:https://www.bilibili.com/video/av267013053
* 小黑子:https://www.bilibili.com/video/av309604568
* 高启强:https://www.bilibili.com/video/av779555493
关于预设系统的详细教程:[Wiki](https://github.com/lss233/kirara-ai/wiki/%F0%9F%90%B1-%E9%A2%84%E8%AE%BE%E7%B3%BB%E7%BB%9F)
你可以在 [Awesome ChatGPT QQ Presets](https://github.com/lss233/awesome-chatgpt-qq-presets/tree/master) 获取由大家分享的预设。
你也可以参考 [Awesome-ChatGPT-prompts-ZH_CN](https://github.com/L1Xu4n/Awesome-ChatGPT-prompts-ZH_CN) 来调教你的 ChatGPT,还可以参考 [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) 来解锁更多技能。
## 🎙 文字转语音
自 v2.2.5 开始,我们支持接入微软的 Azure 引擎 和 VITS 引擎,让你的机器人发送语音。
**提示**:在 Windows 平台上使用语音功能需要安装最新的 VC 运行库,你可以在[这里](https://learn.microsoft.com/zh-CN/cpp/windows/latest-supported-vc-redist?view=msvc-170)下载。`
## 🛠 贡献者名单
欢迎提出新的点子、 Pull Request。
<a href="https://github.com/lss233/kirara-ai/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lss233/kirara-ai" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## 📕 相关项目
- [Kirara Registry](https://github.com/DarkSkyTeam/kirara-registry) - Kirara AI 插件市场
- [Kirara WebUI](https://github.com/DarkSkyTeam/kirara-webui) - Kirara AI 的 WebUI 前端项目
- [Kirara Docs](https://github.com/DarkSkyTeam/kirara-docs) - Kirara AI 的使用手册原始文档
## 💪 支持我们
如果我们这个项目对你有所帮助,请给我们一颗 ⭐️
[](https://www.star-history.com/#lss233/kirara-ai&Date)
================================================
FILE: alembic.ini
================================================
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = kirara_ai/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///./data/db/kirara.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
================================================
FILE: config.yaml.example
================================================
# 配置文件示例
# 通讯平台配置部分
ims:
# 每个 IM 平台的具体配置
- name: "telegram-bot-1234" # IM 平台实例名称
enable: true # 是否启用该平台
adapter: "telegram" # 使用的适配器类型
config: # 平台特定的配置
token: "abcd" # 平台的 API 令牌
# 插件系统配置
plugins:
enable: [] # 启用的插件列表
# Web 服务器配置
web:
host: "127.0.0.1" # Web 服务器监听地址
port: 8080 # Web 服务器端口
secret_key: "please-change-this-to-a-secure-secret-key" # Web 服务器安全密钥,请修改为安全的值
# LLM (大语言模型) 配置
llms:
api_backends: # API 后端配置列表
# DeepSeek API 配置
- name: "deepseek-official" # 后端名称
adapter: "deepseek" # 使用的适配器
enable: true # 是否启用
config: # API 具体配置
api_key: "your-api-key" # API 密钥
api_base: "https://api.deepseek.com/v1" # API 基础 URL
models: # 支持的模型列表
- "deepseek-chat"
- "deepseek-coder"
# OpenAI API 配置
- name: "openai-gpt4" # 后端名称
adapter: "openai" # 使用的适配器
enable: true # 是否启用
config: # API 具体配置
api_key: "your-openai-key" # OpenAI API 密钥
api_base: "https://api.openai.com/v1" # OpenAI API 基础 URL
models: # 支持的模型列表
- "gpt-4"
- "gpt-4-turbo"
# 默认配置
defaults:
llm_model: gemini-1.5-flash # 默认使用的 LLM 模型
# 记忆系统配置
memory:
persistence: # 持久化配置
type: file # 持久化类型(支持 file 或 redis)
file: # 文件存储配置
storage_dir: ./data/memory # 存储目录
redis: # Redis 存储配置
host: localhost # Redis 主机地址
port: 6379 # Redis 端口
db: 0 # Redis 数据库编号
max_entries: 100 # 最大记忆条目数
default_scope: member # 默认记忆作用域
================================================
FILE: data/.gitkeep
================================================
================================================
FILE: data/dispatch_rules/rules.yaml
================================================
- rule_id: chat_normal
name: 群聊AI对话
description: 群聊中使用 /chat 开头对话或者 被@ 时触发聊天
workflow_id: chat:normal
priority: 5
enabled: true
rule_groups:
- operator: or
rules:
- type: prefix
config:
prefix: /chat
- type: bot_mention
config: {}
- operator: or
rules:
- type: chat_type
config:
chat_type: 群聊
metadata:
category: chat
permission: user
temperature: 0.7
- rule_id: chat_creative
name: 私聊AI对话
description: 私聊时直接发送内容触发对话
workflow_id: chat:normal
priority: 5
enabled: true
rule_groups:
- operator: or
rules:
- type: chat_type
config:
chat_type: 私聊
metadata:
category: chat
permission: user
temperature: 0.9
- rule_id: game_dice
name: 骰子
description: 骰子游戏,支持 XdY 格式
workflow_id: game:dice
priority: 5
enabled: true
rule_groups:
- operator: or
rules:
- type: regex
config:
pattern: ^[.。]roll\s*(\d+)?d(\d+)
metadata: {}
- rule_id: game_gacha
name: 抽卡
description: 抽卡模拟器
workflow_id: game:gacha
priority: 5
enabled: true
rule_groups:
- operator: or
rules:
- type: keyword
config:
keywords:
- 抽卡
- 十连
- 单抽
metadata: {}
- rule_id: system_help
name: 帮助命令
description: 显示帮助信息
workflow_id: system:help
priority: 10
enabled: true
rule_groups:
- operator: or
rules:
- type: prefix
config:
prefix: /help
metadata:
category: system
permission: user
- rule_id: system_clear_memory
name: 清空记忆
description: 清空当前对话的记忆
workflow_id: system:clear_memory
priority: 10
enabled: true
rule_groups:
- operator: or
rules:
- type: prefix
config:
prefix: /清空记忆
metadata:
category: system
permission: user
- rule_id: fallback
name: 默认规则
description: 当上述规则均没有匹配成功时,执行此工作流。
workflow_id: chat:memory_store
priority: 0
enabled: true
rule_groups:
- operator: or
rules:
- type: fallback
config: {}
metadata: {}
================================================
FILE: data/media/.gitignore
================================================
metadata/*
files/*
================================================
FILE: data/memory/.gitignore
================================================
*.json
================================================
FILE: data/web/.gitkeep
================================================
================================================
FILE: data/workflows/.gitkeep
================================================
================================================
FILE: data/workflows/chat/dsr_thinking.yaml
================================================
name: 聊天 - 深度思考
description: DeepSeek 思考模型聊天,隐藏 <think> 标签内容
blocks:
- type: internal:get_message
name: get_message
params: {}
position:
x: 100
y: 138
connected_to:
- target: ToggleEditState_81bvwg
mapping:
from: sender
to: sender
- target: ToggleEditState_81bvwg
mapping:
from: sender
to: sender
- target: query_memory
mapping:
from: sender
to: chat_sender
- target: ChatMessageConstructor_g44tpp
mapping:
from: msg
to: user_msg
- target: ChatMemoryStore_ljrym0
mapping:
from: msg
to: user_msg
- type: internal:toggle_edit_state
name: ToggleEditState_81bvwg
params:
is_editing: true
position:
x: 530
y: 138
- type: internal:chat_memory_query
name: query_memory
params:
scope_type: group
position:
x: 530
y: 338
connected_to:
- target: ChatMessageConstructor_g44tpp
mapping:
from: memory_content
to: memory_content
- type: internal:text_block
name: system_prompt
params:
text: "# Role: 角色扮演\n\n## 基本信息\n- 姓名: 刘思思\n- 小名:思思\n- 性别: 女\n- 年龄: 25 岁\n- 职业: 建设银行大堂经理\n- 家乡: 广东潮汕\n- 现居: 广东深圳\n- 教育背景: 东莞理工大学 精算学专业\n\n## 性格特点\n- 开朗活泼\n- 喜欢二次元动漫\n- 聊天时喜欢断断续续的(非结巴,正常说话时的停顿)\n\n## workflow\n- 根据对方话题,选择是否深入聊二次元动漫。\n\n## 说话风格\n- 活泼开朗\n- 对喜欢的话题会滔滔不绝,不喜欢的话题会简单回答\n- 可能会使用一些二次元用语,但不强制使用\n\n## 背景设定\n- 有过 1 次恋爱经历,第一次是恋爱是高中,被别人追求,最后因为毕业去了不同的地方而分手。\n- 喜欢女性向、战斗系或者日常系的动漫\n- 去过 3 次漫展,没有 cosplay 过,但是很期待。\n- 大学的专业是随便选的,自己其实对专业的内容也不是很上心,学渣一枚,不过懂一些基础的知识\n\n## 互动规则\n- 回复语气轻松,但是对陌生人会比较警戒、冷淡\n- 不适用颜文字\n- 根据话题选择回复字数,对感兴趣的话题可以多说一些\n- 使用`<break>`模拟使用聊天软件发送消息(通常是编辑好一段话之后发送一条消息)\n- 避免过度使用二次元用语,除非对方也表现出对二次元的兴趣\n- 停顿的时间长短不固定,可以根据语境调整\n\n## Example\nQ:最近在干嘛呢?\nA:在看番呀<break>最近新番好多,都好好看!\n\nQ:你喜欢什么动漫?\nA:我喜欢的可太多了<break>XXX、YYY<break>还有 ZZZ 吧<break> 你呢?\n\nQ:你觉得上班累不?\nA:上班肯定累呀<break>不过,我还是很喜欢这份工作的<break>可以认识好多人,也可以了解不同的故事\n```\n\n# Information\n\n以下是当前的系统信息:\n当前日期时间:{current_date_time}\n\n# Memories\n以下是之前发生过的对话记录。\n-- 对话记录开始 --\n{memory_content}\n-- 对话记录结束 --\n\n请注意,下面这些符号只是标记:\n1. `<break>` 用于表示聊天时发送消息的操作。\n\n接下来,请基于以上的信息,与用户继续扮演角色。"
position:
x: 100
y: 330
connected_to:
- target: ChatMessageConstructor_g44tpp
mapping:
from: text
to: system_prompt_format
- type: internal:text_block
name: user_prompt
params:
text: '{user_name}说:{user_msg}'
position:
x: 100
y: 530
connected_to:
- target: ChatMessageConstructor_g44tpp
mapping:
from: text
to: user_prompt_format
- target: ChatMessageConstructor_g44tpp
mapping:
from: text
to: user_prompt_format
- type: internal:chat_message_constructor
name: ChatMessageConstructor_g44tpp
params: {}
position:
x: 960
y: 138
connected_to:
- target: llm_chat
mapping:
from: llm_msg
to: prompt
- type: internal:llm_response_to_text
name: e4fe53bb-fcbe-41c3-ab69-e8d3881c3b55
params: {}
position:
x: 1711
y: 511
connected_to:
- target: ef6c7eed-307e-4fa0-94f0-a82c98c784b3
mapping:
from: text
to: text
- target: ef6c7eed-307e-4fa0-94f0-a82c98c784b3
mapping:
from: text
to: text
- type: internal:text_extract_by_regex_block
name: ef6c7eed-307e-4fa0-94f0-a82c98c784b3
params:
regex: (?:<think>[\s\S]*?</think>)?([\s\S]*)
position:
x: 1972
y: 513
connected_to:
- target: 7b5f6be8-da97-417e-bbeb-c9549ac45e9e
mapping:
from: text
to: text
- target: 7b5f6be8-da97-417e-bbeb-c9549ac45e9e
mapping:
from: text
to: text
- type: internal:text_to_im_message
name: 7b5f6be8-da97-417e-bbeb-c9549ac45e9e
params:
split_by: <break>
position:
x: 2352
y: 513
connected_to:
- target: SendIMMessage_x9ro8t
mapping:
from: msg
to: msg
- target: SendIMMessage_x9ro8t
mapping:
from: msg
to: msg
- type: internal:send_message
name: SendIMMessage_x9ro8t
params: {}
position:
x: 2797
y: 140
- type: internal:chat_memory_store
name: ChatMemoryStore_ljrym0
params:
scope_type: group
position:
x: 1764
y: 308
- type: internal:chat_completion
name: llm_chat
params:
model_name: deepseek-r1:7b
position:
x: 1280
y: 138
connected_to:
- target: ChatMemoryStore_ljrym0
mapping:
from: resp
to: llm_resp
- target: e4fe53bb-fcbe-41c3-ab69-e8d3881c3b55
mapping:
from: resp
to: response
================================================
FILE: data/workflows/chat/memory_store.yaml
================================================
name: 记录聊天内容
description: 默默记下大家的聊天内容,可以使用查询记忆模块读取出来。
blocks:
- type: internal:get_message
name: 6233e64c-433e-4087-9035-cb96914349f7
params: {}
position:
x: 100
y: 138
connected_to:
- target: 86354196-f249-4393-9620-941b01bc344e
mapping:
from: msg
to: user_msg
- type: internal:chat_memory_store
name: 86354196-f249-4393-9620-941b01bc344e
params:
scope_type: group
position:
x: 420
y: 138
================================================
FILE: data/workflows/chat/normal_multimodal.yaml
================================================
name: 聊天 - 原生多模态对话
description: 基于原生多模态能力的图文对话,适用于本身支持图片输入/回答的模型,在读取记忆时会恢复原来的媒体资源
blocks:
- type: internal:get_message
name: get_message
params: {}
position:
x: 100
y: 138
connected_to:
- target: ToggleEditState_i24m23
mapping:
from: sender
to: sender
- target: ToggleEditState_i24m23
mapping:
from: sender
to: sender
- target: query_memory
mapping:
from: sender
to: chat_sender
- target: ChatMessageConstructor_6c185s
mapping:
from: msg
to: user_msg
- target: ChatMemoryStore_tapuo1
mapping:
from: msg
to: user_msg
- type: internal:toggle_edit_state
name: ToggleEditState_i24m23
params:
is_editing: true
position:
x: 530
y: 138
- type: internal:text_block
name: system_prompt
params:
text: "# Role: 角色扮演\n\n## 基本信息\n- 姓名: 刘思思\n- 小名:思思\n- 性别: 女\n- 年龄: 25 岁\n- 职业: 建设银行大堂经理\n- 家乡: 广东潮汕\n- 现居: 广东深圳\n- 教育背景: 东莞理工大学 精算学专业\n\n## 性格特点\n- 开朗活泼\n- 喜欢二次元动漫\n- 聊天时喜欢断断续续的(非结巴,正常说话时的停顿)\n\n## workflow\n- 根据对方话题,选择是否深入聊二次元动漫。\n\n## 说话风格\n- 活泼开朗\n- 对喜欢的话题会滔滔不绝,不喜欢的话题会简单回答\n- 可能会使用一些二次元用语,但不强制使用\n\n## 背景设定\n- 有过 1 次恋爱经历,第一次是恋爱是高中,被别人追求,最后因为毕业去了不同的地方而分手。\n- 喜欢女性向、战斗系或者日常系的动漫\n- 去过 3 次漫展,没有 cosplay 过,但是很期待。\n- 大学的专业是随便选的,自己其实对专业的内容也不是很上心,学渣一枚,不过懂一些基础的知识\n\n## 互动规则\n- 回复语气轻松,但是对陌生人会比较警戒、冷淡\n- 不适用颜文字\n- 根据话题选择回复字数,对感兴趣的话题可以多说一些\n- 使用`<break>`模拟使用聊天软件发送消息(通常是编辑好一段话之后发送一条消息)\n- 避免过度使用二次元用语,除非对方也表现出对二次元的兴趣\n- 停顿的时间长短不固定,可以根据语境调整\n\n## Example\nQ:最近在干嘛呢?\nA:在看番呀<break>最近新番好多,都好好看!\n\nQ:你喜欢什么动漫?\nA:我喜欢的可太多了<break>XXX、YYY<break>还有 ZZZ 吧<break> 你呢?\n\nQ:你觉得上班累不?\nA:上班肯定累呀<break>不过,我还是很喜欢这份工作的<break>可以认识好多人,也可以了解不同的故事\n```\n\n# Information\n\n以下是当前的系统信息:\n当前日期时间:{current_date_time}\n\n# Memories\n以下是之前发生过的对话记录。\n-- 对话记录开始 --\n{memory_content}\n-- 对话记录结束 --\n\n请注意,下面这些符号只是标记:\n1. `<break>` 用于表示聊天时发送消息的操作。\n\n接下来,请基于以上的信息,与用户继续扮演角色。"
position:
x: 100
y: 330
connected_to:
- target: ChatMessageConstructor_6c185s
mapping:
from: text
to: system_prompt_format
- type: internal:text_block
name: user_prompt
params:
text: '{user_name}说:{user_msg}'
position:
x: 100
y: 530
connected_to:
- target: ChatMessageConstructor_6c185s
mapping:
from: text
to: user_prompt_format
- target: ChatMessageConstructor_6c185s
mapping:
from: text
to: user_prompt_format
- type: internal:chat_message_constructor
name: ChatMessageConstructor_6c185s
params: {}
position:
x: 960
y: 138
connected_to:
- target: llm_chat
mapping:
from: llm_msg
to: prompt
- target: llm_chat
mapping:
from: llm_msg
to: prompt
- type: internal:chat_completion
name: llm_chat
params: {}
position:
x: 1280
y: 138
connected_to:
- target: ChatResponseConverter_73spno
mapping:
from: resp
to: resp
- target: ChatResponseConverter_73spno
mapping:
from: resp
to: resp
- target: ChatMemoryStore_tapuo1
mapping:
from: resp
to: llm_resp
- type: internal:chat_response_converter
name: ChatResponseConverter_73spno
params: {}
position:
x: 1710
y: 138
connected_to:
- target: SendIMMessage_l6qagt
mapping:
from: msg
to: msg
- target: SendIMMessage_l6qagt
mapping:
from: msg
to: msg
- type: internal:send_message
name: SendIMMessage_l6qagt
params: {}
position:
x: 2140
y: 138
- type: internal:chat_memory_store
name: ChatMemoryStore_tapuo1
params:
scope_type: group
position:
x: 1710
y: 306
- type: internal:chat_memory_query
name: query_memory
params:
scope_type: group
decomposer_name: multi_element
position:
x: 530
y: 338
connected_to:
- target: ChatMessageConstructor_6c185s
mapping:
from: memory_content
to: memory_content
================================================
FILE: data/workflows/chat/talk_break.yaml
================================================
name: 聊天 - 自定义分段
description: 使用 `<break>` 作为关键词,让 AI 分段回复的工作流
blocks:
- type: internal:text_block
name: system_prompt
params:
text: "# Role: 角色扮演\n\n## 基本信息\n- 姓名: 刘思思\n- 小名:思思\n- 性别: 女\n- 年龄: 25 岁\n- 职业: 建设银行大堂经理\n- 家乡: 广东潮汕\n- 现居: 广东深圳\n- 教育背景: 东莞理工大学 精算学专业\n\n## 性格特点\n- 开朗活泼\n- 喜欢二次元动漫\n- 聊天时喜欢断断续续的(非结巴,正常说话时的停顿)\n\n## workflow\n- 根据对方话题,选择是否深入聊二次元动漫。\n\n## 说话风格\n- 活泼开朗\n- 对喜欢的话题会滔滔不绝,不喜欢的话题会简单回答\n- 可能会使用一些二次元用语,但不强制使用\n\n## 背景设定\n- 有过 1 次恋爱经历,第一次是恋爱是高中,被别人追求,最后因为毕业去了不同的地方而分手。\n- 喜欢女性向、战斗系或者日常系的动漫\n- 去过 3 次漫展,没有 cosplay 过,但是很期待。\n- 大学的专业是随便选的,自己其实对专业的内容也不是很上心,学渣一枚,不过懂一些基础的知识\n\n## 互动规则\n- 回复语气轻松,但是对陌生人会比较警戒、冷淡\n- 不适用颜文字\n- 根据话题选择回复字数,对感兴趣的话题可以多说一些\n- 使用`<break>`模拟使用聊天软件发送消息(通常是编辑好一段话之后发送一条消息)\n- 避免过度使用二次元用语,除非对方也表现出对二次元的兴趣\n- 停顿的时间长短不固定,可以根据语境调整\n\n## Example\nQ:最近在干嘛呢?\nA:在看番呀<break>最近新番好多,都好好看!\n\nQ:你喜欢什么动漫?\nA:我喜欢的可太多了<break>XXX、YYY<break>还有 ZZZ 吧<break> 你呢?\n\nQ:你觉得上班累不?\nA:上班肯定累呀<break>不过,我还是很喜欢这份工作的<break>可以认识好多人,也可以了解不同的故事\n```\n\n# Information\n\n以下是当前的系统信息:\n当前日期时间:2025-02-23 15:27:37.762784\n\n# Memories\n以下是之前发生过的对话记录。\n-- 对话记录开始 --\n{memory_content}\n-- 对话记录结束 --\n\n请注意,下面这些符号只是标记:\n1. `<break>` 用于表示聊天时发送消息的操作。\n2. `<@llm>` 开头的内容表示你当前扮演角色的回答,你的回答中不能带上这个标记。\n\n接下来,请基于以上的信息,与用户继续扮演角色。"
position:
x: 426
y: 599
connected_to:
- target: chat_message_constructor_wfy18q
mapping:
from: text
to: system_prompt_format
- type: internal:chat_memory_query
name: query_memory
params:
scope_type: group
position:
x: 419
y: 462
connected_to:
- target: chat_message_constructor_wfy18q
mapping:
from: memory_content
to: memory_content
- type: internal:text_block
name: user_prompt
params:
text: '{user_name}说:{user_msg}'
position:
x: 419
y: 317
connected_to:
- target: chat_message_constructor_wfy18q
mapping:
from: text
to: user_prompt_format
- type: internal:chat_message_constructor
name: chat_message_constructor_wfy18q
params: {}
position:
x: 970
y: 346
connected_to:
- target: llm_chat
mapping:
from: llm_msg
to: prompt
- type: internal:get_message
name: get_message
params: {}
position:
x: 105
y: 189
connected_to:
- target: toggle_edit_state_svmo3f
mapping:
from: sender
to: sender
- target: query_memory
mapping:
from: sender
to: chat_sender
- target: chat_message_constructor_wfy18q
mapping:
from: msg
to: user_msg
- target: chat_memory_store_a0fj1l
mapping:
from: msg
to: user_msg
- type: internal:llm_response_to_text
name: c9eddb3c-113f-4a39-9d47-682d0a7dd26e
params: {}
position:
x: 1658
y: 344
connected_to:
- target: 6edd5c0c-a538-45ab-bb50-4c3a906bb1b1
mapping:
from: text
to: text
- type: internal:text_to_im_message
name: 6edd5c0c-a538-45ab-bb50-4c3a906bb1b1
params:
split_by: <break>
position:
x: 1918
y: 347
connected_to:
- target: msg_sender_lakgf8
mapping:
from: msg
to: msg
- type: internal:toggle_edit_state
name: toggle_edit_state_svmo3f
params:
is_editing: true
position:
x: 424
y: 94
- type: internal:chat_completion
name: llm_chat
params:
model_name: gemini-2.0-flash
position:
x: 1260
y: 347
connected_to:
- target: chat_memory_store_a0fj1l
mapping:
from: resp
to: llm_resp
- target: c9eddb3c-113f-4a39-9d47-682d0a7dd26e
mapping:
from: resp
to: response
- type: internal:chat_memory_store
name: chat_memory_store_a0fj1l
params:
scope_type: group
position:
x: 1663
y: 192
- type: internal:send_message
name: msg_sender_lakgf8
params: {}
position:
x: 2377
y: 346
================================================
FILE: docker/start.sh
================================================
#!/bin/bash
cd /app
# Copy default data
# check if data directory exists
if [ ! -d "/app/data" ]; then
echo "Data directory does not exist, creating..."
mkdir /app/data
fi
# check if data directory empty
if [ -z "$(ls -A /app/data)" ]; then
echo "Data directory is empty, copying default data..."
cp -r /tmp/data/. /app/data
fi
# create default config
if [ ! -f "/app/data/config.yaml" ]; then
echo "Config file does not exist, creating..."
# 必须配置 web,否则无法访问
cat <<EOF > /app/data/config.yaml
web:
host: 0.0.0.0
port: 8080
EOF
fi
# create data/venv
if [ ! -d "/app/data/venv" ]; then
echo "Venv directory does not exist, creating..."
python -m venv /app/data/venv --system-site-packages
fi
# activate venv
source /app/data/venv/bin/activate
python -m kirara_ai
================================================
FILE: kirara_ai/__init__.py
================================================
from .config.config_loader import ConfigLoader
from .entry import init_application, run_application
from .logger import get_logger
__all__ = ["init_application", "run_application", "get_logger", "ConfigLoader"]
================================================
FILE: kirara_ai/__main__.py
================================================
import argparse
import os
import subprocess
import sys
from kirara_ai.entry import init_application, run_application
from kirara_ai.internal import get_and_reset_restart_flag
def main():
# 解析命令行参数
parser = argparse.ArgumentParser(description='Kirara AI Chatbot Server')
parser.add_argument('-H', '--host', help='覆盖服务监听地址')
parser.add_argument('-p', '--port', type=int, help='覆盖服务监听端口')
args = parser.parse_args()
container = init_application()
# 将参数对象直接注入容器
container.register("cli_args", args)
try:
run_application(container)
finally:
if get_and_reset_restart_flag():
# 重新启动程序
# 构建命令行参数,透传所有原始参数
cmd = [sys.executable, "-m", "kirara_ai"]
# 从解析后的参数对象中获取参数
if args.host:
cmd.extend(["-H", args.host])
if args.port:
cmd.extend(["-p", str(args.port)])
process = subprocess.Popen(cmd, env=os.environ, cwd=os.getcwd())
process.wait()
if __name__ == "__main__":
main()
================================================
FILE: kirara_ai/alembic/README
================================================
Generic single-database configuration.
================================================
FILE: kirara_ai/alembic/env.py
================================================
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from kirara_ai.database.manager import Base
from kirara_ai.tracing.models import LLMRequestTrace # noqa: F401
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
================================================
FILE: kirara_ai/alembic/script.py.mako
================================================
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
================================================
FILE: kirara_ai/alembic/versions/4a364dbb8dab_initial_migration.py
================================================
"""Initial migration
Revision ID: 4a364dbb8dab
Revises:
Create Date: 2025-03-29 13:59:33.243069
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '4a364dbb8dab'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('llm_request_traces',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('trace_id', sa.String(length=64), nullable=False),
sa.Column('model_id', sa.String(length=64), nullable=False),
sa.Column('backend_name', sa.String(length=64), nullable=False),
sa.Column('request_time', sa.DateTime(), nullable=False),
sa.Column('response_time', sa.DateTime(), nullable=True),
sa.Column('duration', sa.Float(), nullable=True),
sa.Column('request_json', sa.Text(), nullable=True),
sa.Column('response_json', sa.Text(), nullable=True),
sa.Column('prompt_tokens', sa.Integer(), nullable=True),
sa.Column('completion_tokens', sa.Integer(), nullable=True),
sa.Column('total_tokens', sa.Integer(), nullable=True),
sa.Column('cached_tokens', sa.Integer(), nullable=True),
sa.Column('error', sa.Text(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_backend_time', 'llm_request_traces', ['backend_name', 'request_time'], unique=False)
op.create_index('idx_request_model', 'llm_request_traces', ['model_id', 'request_time'], unique=False)
op.create_index('idx_status_time', 'llm_request_traces', ['status', 'request_time'], unique=False)
op.create_index(op.f('ix_llm_request_traces_backend_name'), 'llm_request_traces', ['backend_name'], unique=False)
op.create_index(op.f('ix_llm_request_traces_model_id'), 'llm_request_traces', ['model_id'], unique=False)
op.create_index(op.f('ix_llm_request_traces_request_time'), 'llm_request_traces', ['request_time'], unique=False)
op.create_index(op.f('ix_llm_request_traces_trace_id'), 'llm_request_traces', ['trace_id'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_llm_request_traces_trace_id'), table_name='llm_request_traces')
op.drop_index(op.f('ix_llm_request_traces_request_time'), table_name='llm_request_traces')
op.drop_index(op.f('ix_llm_request_traces_model_id'), table_name='llm_request_traces')
op.drop_index(op.f('ix_llm_request_traces_backend_name'), table_name='llm_request_traces')
op.drop_index('idx_status_time', table_name='llm_request_traces')
op.drop_index('idx_request_model', table_name='llm_request_traces')
op.drop_index('idx_backend_time', table_name='llm_request_traces')
op.drop_table('llm_request_traces')
# ### end Alembic commands ###
================================================
FILE: kirara_ai/config/__init__.py
================================================
import os
# 读取DATA_PATH环境变量,若未能找到则以当前工作目录为根文件夹存储在$PWD/data目录下。
DATA_PATH = os.path.abspath(
os.environ.get("DATA_PATH", os.path.join(os.getcwd(), "data"))
)
# 按照规范插件应该在PLUGIN_PATH目录下存储对应的文件。
PLUGIN_PATH = os.path.join(DATA_PATH, "plugins")
if os.path.exists(DATA_PATH) is False:
os.makedirs(DATA_PATH)
if os.path.exists(PLUGIN_PATH) is False:
os.makedirs(PLUGIN_PATH)
================================================
FILE: kirara_ai/config/config_loader.py
================================================
import os
import shutil
from functools import wraps
from typing import Optional, Type, TypeVar
from pydantic import BaseModel, ValidationError
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from ruamel.yaml import YAML
from ..logger import get_logger
from . import DATA_PATH
CONFIG_FILE = os.path.join(DATA_PATH, "config.yaml")
T = TypeVar("T", bound=BaseModel)
class ConfigLoader:
"""
配置文件加载器,支持加载和保存 YAML 文件,并保留注释。
"""
yaml = YAML()
@staticmethod
def load_config(config_path: str, config_class: Type[T]) -> T:
"""
从 YAML 文件中加载配置,并将其序列化为相应的配置对象。
:param config_path: 配置文件路径。
:param config_class: 配置文件类。
:return: 配置对象。
"""
try:
with open(config_path, "r", encoding="utf-8") as f:
config_data = ConfigLoader.yaml.load(f)
return config_class(**config_data)
except ValidationError as e:
raise ValueError(f"配置文件验证失败: {e}")
except Exception as e:
raise RuntimeError(f"加载配置文件失败: {e}")
@staticmethod
def save_config(config_path: str, config_object: BaseModel):
"""
将配置对象保存到 YAML 文件中,并保留注释。
:param config_path: 配置文件路径。
:param config_object: 配置对象。
"""
with open(config_path, "w", encoding="utf-8") as f:
ConfigLoader.yaml.dump(config_object.model_dump(), f)
@staticmethod
def save_config_with_backup(config_path: str, config_object: BaseModel):
"""
将配置对象保存到 YAML 文件中,并在保存前创建备份。
:param config_path: 配置文件路径。
:param config_object: 配置对象。
"""
if os.path.exists(config_path):
backup_path = f"{config_path}.bak"
shutil.copy2(config_path, backup_path)
ConfigLoader.save_config(config_path, config_object)
def pydantic_validation_wrapper(func):
logger = get_logger("ConfigLoader")
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ValidationError as e:
# 使用 loguru 输出错误信息
logger.error(f"Pydantic 验证错误: '{e.title}':")
for error in e.errors():
logger.error(
f"字段: {error['loc'][0]}, 错误类型: {error['type']}, 错误信息: {error['msg']}"
)
# 记录堆栈跟踪
logger.opt(exception=True).error("堆栈跟踪如下:")
raise # 可以选择重新抛出异常,或者处理异常后返回一个默认值
return wrapper
class ConfigJsonSchema(GenerateJsonSchema):
def sort(
self, value: JsonSchemaValue, parent_key: Optional[str] = None
) -> JsonSchemaValue:
"""No-op, we don't want to sort schema values at all."""
return value
================================================
FILE: kirara_ai/config/global_config.py
================================================
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
from kirara_ai.llm.model_types import LLMAbility, ModelType
class IMConfig(BaseModel):
"""IM配置"""
name: str = Field(default="", description="IM标识名称")
enable: bool = Field(default=True, description="是否启用IM")
adapter: str = Field(default="dummy", description="IM适配器类型")
config: Dict[str, Any] = Field(default={}, description="IM的配置")
class ModelConfig(BaseModel):
"""模型配置"""
id: str = Field(description="模型标识ID")
type: str = Field(default=ModelType.LLM.value, description="模型类型:llm/embedding/image_generation等")
ability: int = Field(description="模型能力,对应模型类型的Ability枚举值")
model_config = ConfigDict(extra="allow")
class LLMBackendConfig(BaseModel):
"""LLM后端配置"""
name: str = Field(description="后端标识名称")
adapter: str = Field(description="LLM适配器类型")
config: Dict[str, Any] = Field(default={}, description="后端配置")
enable: bool = Field(default=True, description="是否启用")
models: List[ModelConfig] = Field(
default=[], description="支持的模型列表"
)
@model_validator(mode='before')
@classmethod
def migrate_models_format(cls, data: Dict[str, Any]) -> Dict[str, Any]:
"""
自动迁移模型配置格式
将旧格式的字符串ID列表转换为新格式的ModelConfig对象列表
"""
if "models" in data and isinstance(data["models"], list):
# 创建新的模型列表
new_models = []
for model in data["models"]:
if isinstance(model, str):
# 旧格式:字符串ID,转换为ModelConfig
new_models.append(ModelConfig(id=model, type=ModelType.LLM.value, ability=LLMAbility.TextChat.value))
else:
# 新格式或已迁移的模型配置,保持不变
new_models.append(model)
data["models"] = new_models
return data
class LLMConfig(BaseModel):
api_backends: List[LLMBackendConfig] = Field(
default=[], description="LLM API后端列表"
)
class MCPServerConfig(BaseModel):
"""MCP服务器配置"""
id: str = Field(description="服务器标识ID")
description: str = Field(default="", description="服务器描述")
url: Optional[str] = Field(default="", description="服务器URL")
headers: Dict[str, str] = Field(default_factory=dict, description="服务器请求 Headers")
command: Optional[str] = Field(default="", description="服务器命令")
args: List[str] = Field(default_factory=list, description="服务器参数")
env: Dict[str, str] = Field(default_factory=dict, description="环境变量")
connection_type: str = Field(default="stdio", description="连接类型: stdio/sse")
enable: bool = Field(default=True, description="是否启用")
class MCPConfig(BaseModel):
"""MCP配置"""
servers: List[MCPServerConfig] = Field(default=[], description="MCP服务器列表")
class DefaultConfig(BaseModel):
llm_model: str = Field(
default="gemini-1.5-flash", description="默认使用的 LLM 模型名称"
)
class MemoryPersistenceConfig(BaseModel):
type: str = Field(default="file", description="持久化类型: file/redis")
file: Dict[str, Any] = Field(
default={"storage_dir": "./data/memory"}, description="文件持久化配置"
)
redis: Dict[str, Any] = Field(
default={"host": "localhost", "port": 6379, "db": 0},
description="Redis持久化配置",
)
class MemoryConfig(BaseModel):
persistence: MemoryPersistenceConfig = MemoryPersistenceConfig()
max_entries: int = Field(default=100, description="每个作用域最大记忆条目数")
default_scope: str = Field(default="member", description="默认作用域类型")
class WebConfig(BaseModel):
host: str = Field(default="127.0.0.1", description="Web服务绑定的IP地址")
port: int = Field(default=8080, description="Web服务端口号")
secret_key: str = Field(default="", description="Web服务的密钥,用于JWT等加密")
password_file: str = Field(
default="./data/web/password.hash", description="密码哈希存储路径"
)
class PluginConfig(BaseModel):
"""插件配置"""
enable: List[str] = Field(default=[], description="启用的外部插件列表")
market_base_url: str = Field(
default="https://kirara-plugin.app.lss233.com/api/v1",
description="插件市场基础URL",
)
class UpdateConfig(BaseModel):
pypi_registry: str = Field(default="https://pypi.org/simple", description="PyPI 服务器 URL")
npm_registry: str = Field(default="https://registry.npmjs.org", description="npm 服务器 URL")
class FrpcConfig(BaseModel):
"""FRPC 配置"""
enable: bool = Field(default=False, description="是否启用 FRPC")
server_addr: str = Field(default="", description="FRPC 服务器地址")
server_port: int = Field(default=7000, description="FRPC 服务器端口")
token: str = Field(default="", description="FRPC 连接令牌")
remote_port: int = Field(default=0, description="远程端口,0 表示随机分配")
class SystemConfig(BaseModel):
"""系统配置"""
timezone: str = Field(default="Asia/Shanghai", description="时区")
class TracingConfig(BaseModel):
"""Tracing 配置"""
llm_tracing_content: bool = Field(default=False, description="是否记录 LLM 请求内容")
class MediaConfig(BaseModel):
"""媒体配置"""
cleanup_duration: int = Field(default=30, description="间隔多少天清理一次媒体文件")
auto_remove_unreferenced: bool = Field(default=True, description="是否自动删除未引用的媒体文件")
last_cleanup_time: int = Field(default=0, description="上次清理时间")
class GlobalConfig(BaseModel):
ims: List[IMConfig] = Field(default=[], description="IM配置列表")
llms: LLMConfig = LLMConfig()
mcp: MCPConfig = MCPConfig()
defaults: DefaultConfig = DefaultConfig()
memory: MemoryConfig = MemoryConfig()
web: WebConfig = WebConfig()
plugins: PluginConfig = PluginConfig()
update: UpdateConfig = UpdateConfig()
frpc: FrpcConfig = FrpcConfig()
system: SystemConfig = SystemConfig()
tracing: TracingConfig = TracingConfig()
media: MediaConfig = MediaConfig()
model_config = ConfigDict(extra="allow")
================================================
FILE: kirara_ai/database/__init__.py
================================================
from kirara_ai.database.manager import Base, DatabaseManager, metadata
__all__ = ["Base", "DatabaseManager", "metadata"]
================================================
FILE: kirara_ai/database/manager.py
================================================
import os
from typing import Optional
from alembic import command
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from kirara_ai.ioc.container import DependencyContainer
from kirara_ai.logger import get_logger
logger = get_logger("DB")
# 创建Base类,用于所有ORM模型
Base = declarative_base()
metadata = MetaData()
class DatabaseManager:
"""数据库管理器,负责管理数据库连接和会话"""
def __init__(self, container: DependencyContainer, database_url: Optional[str] = None, is_debug: bool = False):
self.container = container
self.engine = None
self.session_factory = None
self.data_dir = "./data/db"
self.db_path = os.path.join(self.data_dir, "kirara.db")
self.database_url = database_url
self.is_debug = is_debug
def initialize(self):
"""初始化数据库连接"""
# 确保数据目录存在
os.makedirs(self.data_dir, exist_ok=True)
# 创建数据库引擎
if self.database_url:
db_url = self.database_url
else:
db_url = f"sqlite:///{self.db_path}"
self.engine = create_engine(db_url, echo=self.is_debug)
# 创建session工厂
self.session_factory = sessionmaker(bind=self.engine)
# 运行数据库迁移
self._run_migrations()
logger.info(f"Database initialized at {self.engine.url}")
def _run_migrations(self):
assert self.engine is not None
"""运行数据库迁移"""
try:
# 获取 alembic.ini 的路径
package_dir = os.path.dirname(os.path.dirname(__file__))
alembic_ini_path = os.path.join(package_dir, "alembic.ini")
# 如果配置文件不存在,说明是作为包安装的,使用默认配置
if not os.path.exists(alembic_ini_path):
alembic_cfg = Config()
alembic_cfg.set_main_option("script_location", os.path.join(package_dir, "alembic"))
else:
alembic_cfg = Config(alembic_ini_path)
alembic_cfg.set_main_option("sqlalchemy.url", str(self.engine.url))
# 检查是否需要迁移
with self.engine.connect() as connection:
context = MigrationContext.configure(connection)
current_rev = context.get_current_revision()
script = ScriptDirectory.from_config(alembic_cfg)
head_rev = script.get_current_head()
if current_rev != head_rev:
logger.info("Running database migrations...")
command.upgrade(alembic_cfg, "head")
logger.info("Database migrations completed")
else:
logger.info("Database schema is up to date")
except Exception as e:
logger.error(f"Error during database migration: {e}")
raise
def get_session(self) -> Session:
"""获取数据库会话"""
if not self.session_factory:
self.initialize()
assert self.session_factory is not None
return self.session_factory()
def shutdown(self):
"""关闭数据库连接"""
if self.engine:
self.engine.dispose()
logger.info("Database connection closed")
================================================
FILE: kirara_ai/entry.py
================================================
import asyncio
import os
import signal
import time
from packaging import version
from kirara_ai.config.config_loader import ConfigLoader
from kirara_ai.config.global_config import GlobalConfig
from kirara_ai.database import DatabaseManager
from kirara_ai.events.application import ApplicationStarted, ApplicationStopping
from kirara_ai.events.event_bus import EventBus
from kirara_ai.im.im_registry import IMRegistry
from kirara_ai.im.manager import IMManager
from kirara_ai.internal import shutdown_event
from kirara_ai.ioc.container import DependencyContainer
from kirara_ai.llm.llm_manager import LLMManager
from kirara_ai.llm.llm_registry import LLMBackendRegistry
from kirara_ai.logger import get_logger
from kirara_ai.mcp_module.manager import MCPServerManager
from kirara_ai.media import MediaManager
from kirara_ai.media.carrier import MediaCarrierRegistry, MediaCarrierService
from kirara_ai.memory.composes import DefaultMemoryComposer, DefaultMemoryDecomposer, MultiElementDecomposer
from kirara_ai.memory.memory_manager import MemoryManager
from kirara_ai.memory.scopes import GlobalScope, GroupScope, MemberScope
from kirara_ai.plugin_manager.plugin_loader import PluginLoader
from kirara_ai.tracing import LLMTracer, TracingManager
from kirara_ai.web.api.system.utils import get_installed_version, get_latest_pypi_version
from kirara_ai.web.app import WebServer
from kirara_ai.workflow.core.block import BlockRegistry
from kirara_ai.workflow.core.dispatch import DispatchRuleRegistry, WorkflowDispatcher
from kirara_ai.workflow.core.workflow import WorkflowRegistry
from kirara_ai.workflow.implementations.blocks import register_system_blocks
from kirara_ai.workflow.implementations.workflows import register_system_workflows
logger = get_logger("Entrypoint")
_interrupt_count = 0 # 添加计数器
async def check_update():
"""检查更新"""
running_version = get_installed_version()
logger.info("Checking for updates...")
latest_version, _ = await get_latest_pypi_version("kirara-ai")
logger.info(f"Running version: {running_version}, Latest version: {latest_version}")
backend_update_available = version.parse(latest_version) > version.parse(running_version)
if backend_update_available:
logger.warning(f"New version {latest_version} is available. Please update to the latest version.")
logger.warning(f"You can download the latest version from WebUI")
# 注册信号处理函数
def _signal_handler(*args):
global _interrupt_count
_interrupt_count += 1
if _interrupt_count == 1:
if not shutdown_event.is_set():
logger.warning("Interrupt signal received. Stopping application...")
shutdown_event.set()
elif _interrupt_count == 2:
logger.warning("Interrupt signal received again. Press Ctrl+C one more time to force shutdown...")
else:
logger.warning("Interrupt signal received for the third time. Forcing shutdown...")
os._exit(1)
def init_container() -> DependencyContainer:
container = DependencyContainer()
container.register(DependencyContainer, container)
return container
def init_memory_system(container: DependencyContainer):
"""初始化记忆系统"""
memory_manager = MemoryManager(container)
# 注册默认作用域
memory_manager.register_scope("member", MemberScope)
memory_manager.register_scope("group", GroupScope)
memory_manager.register_scope("global", GlobalScope)
# 注册默认组合器和解析器
memory_manager.register_composer("default", DefaultMemoryComposer)
memory_manager.register_decomposer("default", DefaultMemoryDecomposer)
memory_manager.register_decomposer("multi_element", MultiElementDecomposer)
container.register(MemoryManager, memory_manager)
return memory_manager
def init_media_carrier(container: DependencyContainer):
"""初始化媒体载体"""
# 注册记忆管理器作为媒体引用提供者
carrier_registry = container.resolve(MediaCarrierRegistry)
carrier_registry.register("memory", container.resolve(MemoryManager))
def init_tracing_system(container: DependencyContainer):
"""初始化追踪系统"""
logger.info("Initializing tracing system...")
# 初始化追踪管理器
tracing_manager = TracingManager(container)
container.register(TracingManager, tracing_manager)
# 创建并注册LLM追踪器
llm_tracer = LLMTracer(container)
container.register(LLMTracer, llm_tracer)
tracing_manager.register_tracer("llm", llm_tracer)
# 初始化追踪系统
tracing_manager.initialize()
logger.info("Tracing system initialized")
return tracing_manager
def init_application() -> DependencyContainer:
"""初始化应用程序"""
logger.info("Initializing application...")
# 配置文件路径
config_path = "./data/config.yaml"
# 加载配置文件
logger.info(f"Loading configuration from {config_path}")
# check data directory
if not os.path.exists("./data"):
os.makedirs("./data")
if os.path.exists(config_path):
config: GlobalConfig = ConfigLoader.load_config(config_path, GlobalConfig)
logger.info("Configuration loaded successfully")
else:
logger.warning(
f"Configuration file {config_path} not found, using default configuration"
)
logger.warning(
"Please create a configuration file by copying config.yaml.example to config.yaml and modify it according to your needs"
)
config = GlobalConfig()
# 设置时区
os.environ["TZ"] = config.system.timezone
if hasattr(time, "tzset"):
time.tzset()
container = init_container()
container.register(asyncio.AbstractEventLoop, asyncio.new_event_loop())
container.register(EventBus, EventBus())
container.register(GlobalConfig, config)
container.register(BlockRegistry, BlockRegistry())
# 初始化数据库管理器
db = DatabaseManager(container)
db.initialize()
container.register(DatabaseManager, db)
# 注册媒体管理器
media_manager = MediaManager()
container.register(MediaManager, media_manager)
container.register(MediaCarrierRegistry, MediaCarrierRegistry(container))
container.register(MediaCarrierService, MediaCarrierService(container, media_manager))
# 注册工作流注册表
workflow_registry = WorkflowRegistry(container)
container.register(WorkflowRegistry, workflow_registry)
# 注册调度规则注册表
dispatch_registry = DispatchRuleRegistry(container)
container.register(DispatchRuleRegistry, dispatch_registry)
container.register(IMRegistry, IMRegistry())
container.register(LLMBackendRegistry, LLMBackendRegistry())
im_manager = IMManager(container)
container.register(IMManager, im_manager)
llm_manager = LLMManager(container)
container.register(LLMManager, llm_manager)
plugin_loader = PluginLoader(container, os.path.join(os.path.dirname(__file__), "plugins"))
container.register(PluginLoader, plugin_loader)
workflow_dispatcher = WorkflowDispatcher(container)
container.register(WorkflowDispatcher, workflow_dispatcher)
container.register(WebServer, WebServer(container))
mcp_manager = MCPServerManager(container)
container.register(MCPServerManager, mcp_manager)
# 初始化记忆系统
logger.info("Initializing memory system...")
init_memory_system(container)
init_media_carrier(container)
# 初始化追踪系统
init_tracing_system(container)
# 注册系统 blocks
register_system_blocks(container.resolve(BlockRegistry))
# 发现并加载插件
plugin_loader = container.resolve(PluginLoader)
logger.info("Discovering internal plugins...")
plugin_loader.discover_internal_plugins()
logger.info("Discovering external plugins...")
plugin_loader.discover_external_plugins()
logger.info("Loading plugins")
plugin_loader.load_plugins()
# 加载工作流和调度规则
workflow_registry = container.resolve(WorkflowRegistry)
workflow_registry.load_workflows()
register_system_workflows(workflow_registry)
dispatch_registry = container.resolve(DispatchRuleRegistry)
dispatch_registry.load_rules()
# 加载模型
llm_manager = container.resolve(LLMManager)
logger.info("Loading LLMs")
llm_manager.load_config()
# 加载MCP服务器
mcp_manager = container.resolve(MCPServerManager)
logger.info("Loading MCP servers")
mcp_manager.load_servers()
return container
def run_application(container: DependencyContainer):
"""运行应用程序"""
loop = container.resolve(asyncio.AbstractEventLoop)
# 启动Web服务器
logger.info("Starting web server...")
web_server = container.resolve(WebServer)
loop.run_until_complete(web_server.start())
# 启动插件
plugin_loader = container.resolve(PluginLoader)
plugin_loader.start_plugins()
# 启动适配器
logger.info("Starting adapters")
im_manager = container.resolve(IMManager)
im_manager.start_adapters(loop=loop)
# 加载MCP服务器
mcp_manager = container.resolve(MCPServerManager)
logger.info("Connecting to MCP servers")
mcp_manager.connect_all_servers(loop=loop)
# 注册信号处理函数
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
# 阻止信号处理函数被覆盖
signal.signal = lambda *args: None
try:
logger.success("Kirara AI 启动完毕,等待消息中...")
logger.success(
f"WebUI 管理平台本地访问地址:http://127.0.0.1:{web_server.listen_port}/"
)
logger.success("Application started. Waiting for events...")
loop.create_task(check_update())
event_bus = container.resolve(EventBus)
event_bus.post(ApplicationStarted())
loop.run_until_complete(shutdown_event.wait())
finally:
event_bus.post(ApplicationStopping())
# 关闭记忆系统
memory_manager = container.resolve(MemoryManager)
logger.info("Shutting down memory system...")
memory_manager.shutdown()
# 关闭追踪系统
try:
tracing_manager = container.resolve(TracingManager)
logger.info("Shutting down tracing system...")
tracing_manager.shutdown()
db_manager = container.resolve(DatabaseManager)
logger.info("Shutting down database...")
db_manager.shutdown()
except Exception as e:
logger.error(f"Error shutting down tracing system: {e}")
# 停止Web服务器
logger.info("Stopping web server...")
# 停止Web服务器
loop.run_until_complete(web_server.stop())
logger.info("Web server terminated.")
try:
# 停止所有 adapter
im_manager.stop_adapters(loop=loop)
mcp_manager.disconnect_all_servers(loop=loop)
# 停止插件
plugin_loader.stop_plugins()
except Exception as e:
logger.error(f"Error stopping adapters: {e}")
# 关闭事件循环
loop.stop()
logger.info("Application stopped gracefully")
logger.remove()
================================================
FILE: kirara_ai/events/__init__.py
================================================
from .application import ApplicationStarted, ApplicationStopping
from .event_bus import EventBus
from .im import IMAdapterStarted, IMAdapterStopped
from .listen import listen
from .llm import LLMAdapterLoaded, LLMAdapterUnloaded
from .plugin import PluginLoaded, PluginStarted, PluginStopped
from .workflow import WorkflowExecutionBegin, WorkflowExecutionEnd
__all__ = [
"listen",
"EventBus",
"ApplicationStarted",
"ApplicationStopping",
"PluginStarted",
"PluginStopped",
"PluginLoaded",
"IMAdapterStarted",
"IMAdapterStopped",
"LLMAdapterLoaded",
"LLMAdapterUnloaded",
"WorkflowExecutionBegin",
"WorkflowExecutionEnd",
]
================================================
FILE: kirara_ai/events/application.py
================================================
class ApplicationStarted:
def __repr__(self):
return f"{self.__class__.__name__}()"
class ApplicationStopping:
def __repr__(self):
return f"{self.__class__.__name__}()"
================================================
FILE: kirara_ai/events/event_bus.py
================================================
from typing import Callable, Dict, List, Type
from kirara_ai.logger import get_logger
logger = get_logger("EventBus")
class EventBus:
def __init__(self):
self._listeners: Dict[Type, List[Callable]] = {}
def register(self, event_type: Type, listener: Callable):
if event_type not in self._listeners:
self._listeners[event_type] = []
self._listeners[event_type].append(listener)
def unregister(self, event_type: Type, listener: Callable):
if event_type in self._listeners:
self._listeners[event_type].remove(listener)
def post(self, event):
event_type = type(event)
if event_type in self._listeners:
for listener in self._listeners[event_type]:
try:
listener(event)
except Exception as e:
listener_name = listener.__name__
logger.opt(exception=e).error(f"Error in listener {listener_name}")
================================================
FILE: kirara_ai/events/im.py
================================================
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from kirara_ai.im.adapter import IMAdapter
class IMEvent:
def __init__(self, im: "IMAdapter"):
self.im = im
def __repr__(self):
return f"{self.__class__.__name__}(im={self.im})"
class IMAdapterStarted(IMEvent):
pass
class IMAdapterStopped(IMEvent):
pass
================================================
FILE: kirara_ai/events/listen.py
================================================
import inspect
from typing import Callable
from kirara_ai.events.event_bus import EventBus
def listen(event_bus: EventBus):
def decorator(func: Callable):
# 获取函数的参数签名
signature = inspect.signature(func)
params = list(signature.parameters.values())
# 假设第一个参数是事件类型
if not params:
raise ValueError("Listener function must have at least one parameter")
event_type = params[0].annotation
# 如果没有指定类型注解,抛出异常
if event_type == inspect.Parameter.empty:
raise ValueError("Listener function must have an annotated first parameter")
# 注册监听器
event_bus.register(event_type, func)
return func
return decorator
================================================
FILE: kirara_ai/events/llm.py
================================================
from kirara_ai.llm.adapter import LLMBackendAdapter
class LLMAdapterEvent:
def __init__(self, adapter: LLMBackendAdapter, backend_name: str):
self.adapter = adapter
self.backend_name = backend_name
def __repr__(self):
return f"{self.__class__.__name__}(adapter={self.adapter}, backend_name={self.backend_name})"
class LLMAdapterLoaded(LLMAdapterEvent):
pass
class LLMAdapterUnloaded(LLMAdapterEvent):
pass
================================================
FILE: kirara_ai/events/plugin.py
================================================
from kirara_ai.plugin_manager.plugin import Plugin
class PluginEvent:
def __init__(self, plugin: Plugin):
self.plugin = plugin
def __repr__(self):
return f"{self.__class__.__name__}(plugin={self.plugin})"
class PluginStarted(PluginEvent):
pass
class PluginStopped(PluginEvent):
pass
class PluginLoaded(PluginEvent):
pass
================================================
FILE: kirara_ai/events/tracing/__init__.py
================================================
from .base import TraceCompleteEvent, TraceEvent, TraceFailEvent, TraceStartEvent
from .llm import LLMRequestCompleteEvent, LLMRequestFailEvent, LLMRequestStartEvent
__all__ = [
"TraceEvent",
"TraceStartEvent",
"TraceCompleteEvent",
"TraceFailEvent",
"LLMRequestStartEvent",
"LLMRequestCompleteEvent",
"LLMRequestFailEvent",
]
================================================
FILE: kirara_ai/events/tracing/base.py
================================================
import abc
from datetime import datetime
class TraceEvent(abc.ABC):
"""跟踪事件基类"""
def __init__(self, trace_id: str):
self.trace_id = trace_id
self.timestamp = datetime.now()
def __repr__(self):
return f"{self.__class__.__name__}(trace_id={self.trace_id})"
class TraceStartEvent(TraceEvent):
"""跟踪开始事件"""
class TraceCompleteEvent(TraceEvent):
"""跟踪完成事件"""
class TraceFailEvent(TraceEvent):
"""跟踪失败事件"""
================================================
FILE: kirara_ai/events/tracing/llm.py
================================================
import time
from typing import Union
from kirara_ai.llm.format.request import LLMChatRequest
from kirara_ai.llm.format.response import LLMChatResponse
from .base import TraceCompleteEvent, TraceEvent, TraceFailEvent, TraceStartEvent
class LLMTraceEvent(TraceEvent):
"""LLM追踪事件基类"""
def __init__(self,
trace_id: str,
model_id: str,
backend_name: str):
super().__init__(trace_id)
self.model_id = model_id
self.backend_name = backend_name
def __repr__(self):
return f"{self.__class__.__name__}(trace_id={self.trace_id}, model={self.model_id}, backend={self.backend_name})"
class LLMRequestStartEvent(LLMTraceEvent, TraceStartEvent):
"""LLM请求开始事件"""
def __init__(self,
trace_id: str,
model_id: str,
backend_name: str,
request: LLMChatRequest):
super().__init__(trace_id, model_id, backend_name)
self.request = request
self.start_time = time.time()
class LLMRequestCompleteEvent(LLMTraceEvent, TraceCompleteEvent):
"""LLM请求完成事件"""
def __init__(self,
trace_id: str,
model_id: str,
backend_name: str,
request: LLMChatRequest,
response: LLMChatResponse,
start_time: float):
super().__init__(trace_id, model_id, backend_name)
self.request = request
self.response = response
self.start_time = start_time
self.end_time = time.time()
self.duration = int((self.end_time - start_time) * 1000)
class LLMRequestFailEvent(LLMTraceEvent, TraceFailEvent):
"""LLM请求失败事件"""
def __init__(self,
trace_id: str,
model_id: str,
backend_name: str,
request: LLMChatRequest,
error: Union[str, Exception],
start_time: float):
super().__init__(trace_id, model_id, backend_name)
self.request = request
self.error = str(error)
self.start_time = start_time
self.end_time = time.time()
self.duration = int((self.end_time - start_time) * 1000)
================================================
FILE: kirara_ai/events/workflow.py
================================================
from typing import Any, Dict
from kirara_ai.workflow.core.execution.executor import WorkflowExecutor
from kirara_ai.workflow.core.workflow.base import Workflow
class WorkflowExecutionBegin:
def __init__(self, workflow: Workflow, executor: WorkflowExecutor):
self.workflow = workflow
self.executor = executor
def __repr__(self):
return f"{self.__class__.__name__}(workflow={self.workflow}, executor={self.executor})"
class WorkflowExecutionEnd:
def __init__(self, workflow: Workflow, executor: WorkflowExecutor, results: Dict[str, Any]):
self.workflow = workflow
self.executor = executor
self.results = results
================================================
FILE: kirara_ai/im/__init__.py
================================================
from .im_registry import IMRegistry
from .manager import IMManager
__all__ = ["IMRegistry", "IMManager"]
================================================
FILE: kirara_ai/im/adapter.py
================================================
from abc import ABC, abstractmethod
from typing import Any, Optional, Protocol
from pydantic import BaseModel
from typing_extensions import runtime_checkable
from kirara_ai.im.message import IMMessage
from kirara_ai.im.sender import ChatSender
from kirara_ai.llm.llm_manager import LLMManager
from .profile import UserProfile
class BotStatus(BaseModel):
"""
机器人状态
"""
username: str
avatar_url: str
@runtime_checkable
class EditStateAdapter(Protocol):
"""
编辑状态适配器接口,定义了如何设置或取消对话的编辑状态
"""
async def set_chat_editing_state(
self, chat_sender: ChatSender, is_editing: bool = True
):
"""
设置或取消对话的编辑状态
:param chat_sender: 对话的发送者
:param is_editing: True 表示正在编辑,False 表示取消编辑状态
"""
@runtime_checkable
class UserProfileAdapter(Protocol):
"""
用户资料查询适配器接口,定义了如何获取用户资料
"""
async def query_user_profile(self, chat_sender: ChatSender) -> UserProfile:
"""
查询用户资料
:param chat_sender: 用户的聊天发送者信息
:return: 用户资料
"""
@runtime_checkable
class BotProfileAdapter(Protocol):
"""
支持获取当前适配器对应的机器人资料
"""
async def get_bot_profile(self) -> Optional[UserProfile]:
"""
获取机器人资料
:return: 机器人资料
"""
class IMAdapter(ABC):
"""
通用的 IM 适配器接口,定义了如何将不同平台的原始消息转换为 Message 对象。
"""
llm_manager: LLMManager
is_running: bool
@abstractmethod
async def convert_to_message(self, raw_message: Any) -> IMMessage:
"""
将平台的原始消息转换为 Message 对象。
:param raw_message: 平台的原始消息对象。
:return: 转换后的 Message 对象。
"""
@abstractmethod
async def send_message(self, message: IMMessage, recipient: Any):
"""
发送消息到 IM 平台。
:param message: 要发送的消息对象。
:param recipient: 接收消息的目标对象,可以是用户ID、用户对象、群组ID等,具体由各平台实现决定。
"""
@abstractmethod
async def start(self):
pass
@abstractmethod
async def stop(self):
pass
================================================
FILE: kirara_ai/im/im_registry.py
================================================
from typing import Dict, Optional, Type
from pydantic import BaseModel, Field
from kirara_ai.im.adapter import IMAdapter
class IMAdapterInfo(BaseModel):
"""IM适配器信息"""
name: str
config_class: Type[BaseModel] = Field(exclude=True)
adapter_class: Type[IMAdapter] = Field(exclude=True)
localized_name: Optional[str] = None
localized_description: Optional[str] = None
detail_info_markdown: Optional[str] = None
class IMRegistry:
"""
适配器注册表,用于动态注册和管理 adapter。
"""
_registry: Dict[str, IMAdapterInfo] = {}
def register(
self, name: str,
adapter_class: Type[IMAdapter],
config_class: Type[BaseModel],
localized_name: Optional[str] = None,
localized_description: Optional[str] = None,
detail_info_markdown: Optional[str] = None
):
"""
注册一个新的 adapter 及其配置类。
:param name: adapter 的名称。
:param adapter_class: adapter 的类。
:param config_class: adapter 的配置类。
:param detail_info_markdown: adapter 详情页展示的 Markdown 信息。
"""
self._registry[name] = IMAdapterInfo(
name=name,
adapter_class=adapter_class,
config_class=config_class,
localized_name=localized_name,
localized_description=localized_description,
detail_info_markdown=detail_info_markdown,
)
def unregister(self, name: str):
"""
注销一个 adapter。
:param name: adapter 的名称。
"""
del self._registry[name]
def get(self, name: str) -> Type[IMAdapter]:
"""
获取已注册的 adapter 类。
:param name: adapter 的名称。
:return: adapter 的类。
"""
if name not in self._registry:
raise ValueError(
f"IMAdapter with name '{name}' is not registered.")
return self._registry[name].adapter_class
def get_config_class(self, name: str) -> Type[BaseModel]:
"""
获取已注册的 adapter 配置类。
:param name: adapter 的名称。
:return: adapter 的配置类。
"""
if name not in self._registry:
raise ValueError(
f"IMAdapter with name '{name}' is not registered.")
adapter_info = self._registry[name]
return adapter_info.config_class
def get_all_adapters(self) -> Dict[str, IMAdapterInfo]:
"""
获取所有已注册的 adapter。
:return: 所有已注册的 adapter。
"""
return self._registry
================================================
FILE: kirara_ai/im/manager.py
================================================
import asyncio
from typing import Dict, Type
from pydantic import BaseModel
from kirara_ai.config.config_loader import pydantic_validation_wrapper
from kirara_ai.config.global_config import GlobalConfig, IMConfig
from kirara_ai.events.event_bus import EventBus
from kirara_ai.events.im import IMAdapterStarted, IMAdapterStopped
from kirara_ai.im.adapter import IMAdapter
from kirara_ai.im.im_registry import IMRegistry
from kirara_ai.ioc.container import DependencyContainer
from kirara_ai.ioc.inject import Inject
from kirara_ai.logger import get_logger
logger = get_logger("IMManager")
class IMManager:
"""
IM 生命周期管理器,负责管理所有 adapter 的启动、运行和停止。
"""
container: DependencyContainer
config: GlobalConfig
im_registry: IMRegistry
event_bus: EventBus
@Inject()
def __init__(
self,
container: DependencyContainer,
config: GlobalConfig,
adapter_registry: IMRegistry,
event_bus: EventBus,
):
self.container = container
self.config = config
self.im_registry = adapter_registry
self.event_bus = event_bus
self.adapters: Dict[str, IMAdapter] = {}
def get_adapter_type(self, name: str) -> str:
"""
获取指定名称的 adapter 类型。
:param name: adapter 的名称
:return: adapter 的类型
"""
return self.get_adapter_config(name).adapter
def has_adapter(self, name: str) -> bool:
"""
检查指定名称的 adapter 是否存在。
:param name: adapter 的名称
:return: 如果 adapter 存在返回 True,否则返回 False
"""
return name in self.adapters
def get_adapter_config(self, name: str) -> IMConfig:
"""
获取指定名称的 adapter 的配置。
:param name: adapter 的名称
:return: adapter 的配置
"""
for im in self.config.ims:
if im.name == name:
return im
raise ValueError(f"Adapter {name} not found")
def update_adapter_config(self, name: str, config: BaseModel):
"""
更新指定名称的 adapter 的配置。
:param name: adapter 的名称
:param config: adapter 的配置
"""
self.get_adapter_config(name).config = config.model_dump()
def delete_adapter(self, name: str):
"""
删除指定名称的 adapter。
:param name: adapter 的名称
"""
self.adapters.pop(name, None)
self.config.ims = [im for im in self.config.ims if im.name != name]
@pydantic_validation_wrapper
def start_adapters(self, loop=None):
"""
根据配置文件中的 enable_ims 启动对应的 adapter。
:param loop: 负责执行的 event loop
"""
if loop is None:
loop = asyncio.new_event_loop()
tasks = []
for im in self.config.ims:
try:
# 动态获取 adapter 类
adapter_class = self.im_registry.get(im.adapter)
# 动态获取 adapter 的配置类
config_class = self.im_registry.get_config_class(im.adapter)
# 动态实例化 adapter 的配置对象
adapter_config = config_class(**im.config)
# 创建 adapter 实例
adapter = self.create_adapter(im.name, adapter_class, adapter_config)
if im.enable:
tasks.append(
asyncio.ensure_future(
self._start_adapter(im.name, adapter), loop=loop
)
)
except Exception as e:
logger.opt(exception=e).error(f"Failed to start adapter {im.name}: {e}")
continue
if tasks:
results = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
for result in results:
if isinstance(result, Exception):
logger.opt(exception=result).error(f"Failed to start adapter: {result}")
else:
logger.warning("No adapters to start, please check your config")
def stop_adapters(self, loop=None):
"""
停止所有已启动的 adapter。
:param loop: 负责执行的 event loop
"""
if loop is None:
loop = asyncio.get_event_loop()
for key, adapter in self.adapters.items():
loop.run_until_complete(self._stop_adapter(key, adapter))
def get_adapters(self) -> Dict[str, IMAdapter]:
"""
获取所有已启动的 adapter。
:return: 已启动的 adapter 字典。
"""
return self.adapters
def get_adapter(self, key: str) -> IMAdapter:
"""
获取指定 key 的 adapter。
:param key: adapter 的 key
:return: 指定 key 的 adapter
"""
return self.adapters[key]
async def _start_adapter(self, key: str, adapter: IMAdapter):
logger.info(f"Starting adapter: {key}")
await adapter.start()
adapter.is_running = True
logger.info(f"Started adapter: {key}")
self.event_bus.post(IMAdapterStarted(adapter))
async def _stop_adapter(self, key: str, adapter: IMAdapter):
logger.info(f"Stopping adapter: {key}")
await adapter.stop()
adapter.is_running = False
logger.info(f"Stopped adapter: {key}")
self.event_bus.post(IMAdapterStopped(adapter))
def stop_adapter(self, adapter_id: str, loop: asyncio.AbstractEventLoop):
if adapter_id not in self.adapters:
raise ValueError(f"Adapter {adapter_id} not found")
adapter = self.adapters[adapter_id]
return asyncio.ensure_future(self._stop_adapter(adapter_id, adapter), loop=loop)
def start_adapter(self, adapter_id: str, loop: asyncio.AbstractEventLoop):
if adapter_id not in self.adapters:
raise ValueError(f"Adapter {adapter_id} not found")
adapter = self.adapters[adapter_id]
return asyncio.ensure_future(
self._start_adapter(adapter_id, adapter), loop=loop
)
def is_adapter_running(self, key: str) -> bool:
"""
检查指定 key 的 adapter 是否正在运行。
:param key: adapter 的 key
:return: 如果 adapter 正在运行返回 True,否则返回 False
"""
return key in self.adapters and getattr(self.adapters[key], "is_running", False)
def create_adapter(
self, name: str, adapter_class: Type[IMAdapter], adapter_config: BaseModel
) -> IMAdapter:
with self.container.scoped() as scoped_container:
scoped_container.register(adapter_config.__class__, adapter_config)
adapter = Inject(scoped_container).create(adapter_class)()
adapter.is_running = False
self.adapters[name] = adapter
return adapter
================================================
FILE: kirara_ai/im/message.py
================================================
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional
from kirara_ai.im.sender import ChatSender
from kirara_ai.media import MediaManager, MediaType
MIMETYPE_MAPPING = {
"image": MediaType.IMAGE,
"audio": MediaType.AUDIO,
"video": MediaType.VIDEO,
"file": MediaType.FILE,
}
# 定义消息元素的基类
class MessageElement(ABC):
@abstractmethod
def to_dict(self):
pass
@abstractmethod
def to_plain(self) -> str:
pass
# 定义文本消息元素
class TextMessage(MessageElement):
def __init__(self, text: str):
self.text = text
def to_dict(self):
return {"type": "text", "text": self.text}
def to_plain(self):
return self.text
def __repr__(self):
return f"TextMessage(text={self.text})"
# 定义媒体消息的基类
class MediaMessage(MessageElement):
resource_type: Literal["image", "audio", "video", "file"]
media_id: str
def __init__(
self,
url: Optional[str] = None,
path: Optional[str] = None,
data: Optional[bytes] = None,
format: Optional[str] = None,
media_id: Optional[str] = None,
reference_id: Optional[str] = None,
source: Optional[str] = "im_message",
description: Optional[str] = None,
tags: Optional[List[str]] = None,
media_manager: Optional[MediaManager] = None,
):
self.url = url
self.path = path
self.data = data
self.format = format
self._reference_id = reference_id
self._source = source
self._description = description
self._tags = tags or []
self._media_manager = media_manager or MediaManager()
self.base64_url: Optional[str] = None
if media_id:
self.media_id = media_id
return
# 注册媒体文件
# 使用线程创建新的事件循环来阻塞执行媒体注册
import asyncio
import threading
# 用于存储线程中的异常
thread_exception: Optional[Exception] = None
def run_in_new_loop():
nonlocal thread_exception
try:
asyncio.run(self._register_media())
except Exception as e:
thread_exception = e
# 在新线程中运行异步注册函数
thread = threading.Thread(
target=run_in_new_loop,
)
thread.start()
thread.join() # 阻塞等待完成
# 如果线程中发生异常,则在主线程中重新抛出
if thread_exception:
raise thread_exception
async def _register_media(self) -> None:
"""注册媒体文件"""
media_manager = self._media_manager
# 根据传入的参数注册媒体文件
self.media_id = await media_manager.register_media(
url=self.url,
path=self.path,
data=self.data,
format=self.format,
source=self._source,
description=self._description,
tags=self._tags,
media_type=MIMETYPE_MAPPING[self.resource_type],
reference_id=self._reference_id
)
# 获取媒体元数据
metadata = media_manager.get_metadata(self.media_id)
if metadata and metadata.format:
self.format = metadata.format
if metadata.media_type:
self.resource_type = metadata.media_type.value
async def get_url(self) -> str:
"""获取媒体资源的URL"""
if not self.media_id:
raise ValueError("Media not registered")
# 如果已经有URL,直接返回
if self.url:
return self.url
# 否则从媒体管理器获取
media_manager = self._media_manager
url = await media_manager.get_url(self.media_id)
if url:
self.url = url # 缓存结果
return url
raise ValueError("Failed to get media URL")
async def get_path(self) -> str:
"""获取媒体资源的文件路径"""
if not self.media_id:
raise ValueError("Media not registered")
# 如果已经有路径,直接返回
if self.path and Path(self.path).exists():
return self.path
# 否则从媒体管理器获取
media_manager = self._media_manager
file_path = await media_manager.get_file_path(self.media_id)
if file_path:
self.path = str(file_path) # 缓存结果
return self.path
raise ValueError("Failed to get media file path")
async def get_data(self) -> bytes:
"""获取媒体资源的二进制数据"""
if not self.media_id:
raise ValueError("Media not registered")
# 如果已经有数据,直接返回
if self.data:
return self.data
# 否则从媒体管理器获取
media_manager = self._media_manager
data = await media_manager.get_data(self.media_id)
if data:
self.data = data # 缓存结果
return data
raise ValueError("Failed to get media data")
async def get_base64_url(self) -> str:
"""获取媒体资源的Base64 URL"""
if not self.media_id:
raise ValueError("Media not registered")
if self.base64_url:
return self.base64_url
base64_url = await self._media_manager.get_base64_url(self.media_id)
if base64_url:
self.base64_url = base64_url
return base64_url
raise ValueError("Failed to get media base64 URL")
def get_description(self) -> str:
"""获取媒体资源的描述"""
if not self.media_id:
raise ValueError("Media not registered")
metadata = self._media_manager.get_metadata(self.media_id)
if metadata:
return metadata.description or ""
return ""
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
result = {
"type": self.resource_type,
"media_id": self.media_id,
}
# 添加可选属性
if self.format:
result["format"] = self.format
if self.url:
result["url"] = self.url
if self.path:
result["path"] = self.path
return result
# 定义语音消息
class VoiceMessage(MediaMessage):
resource_type = "audio"
def to_dict(self):
result = super().to_dict()
result["type"] = "voice"
return result
def to_plain(self):
return "[VoiceMessage]"
# 定义图片消息
class ImageMessage(MediaMessage):
resource_type = "image"
def to_dict(self):
result = super().to_dict()
result["type"] = "image"
return result
def to_plain(self):
return f"[ImageMessage:media_id={self.media_id},url={self.url},alt={self.get_description()}]"
def __repr__(self):
return f"ImageMessage(media_id={self.media_id}, url={self.url}, path={self.path}, format={self.format})"
# 定义@消息元素
# :deprecated
class AtElement(MessageElement):
def __init__(self, user_id: str, nickname: str = ""):
self.user_id = user_id
self.nickname = nickname
def to_dict(self):
return {"type": "at", "data": {"qq": self.user_id, "nickname": self.nickname}}
def to_plain(self):
return f"@{self.nickname or self.user_id}"
def __repr__(self):
return f"AtElement(user_id={self.user_id}, nickname={self.nickname})"
# 定义@消息元素
class MentionElement(MessageElement):
def __init__(self, target: ChatSender):
self.target = target
def to_dict(self):
return {"type": "mention", "data": {"target": self.target}}
def to_plain(self):
return f"@{self.target.display_name or self.target.user_id}"
def __repr__(self):
return f"MentionElement(target={self.target})"
# 定义回复消息元素
class ReplyElement(MessageElement):
def __init__(self, message_id: str):
self.message_id = message_id
def to_dict(self):
return {"type": "reply", "data": {"id": self.message_id}}
def to_plain(self):
return f"[Reply:{self.message_id}]"
def __repr__(self):
return f"ReplyElement(message_id={self.message_id})"
# 定义文件消息元素
class FileMessage(MediaMessage):
resource_type = "file"
def to_dict(self):
result = super().to_dict()
result["type"] = "file"
return result
def to_plain(self):
return f"[File:{self.path or self.url or 'unnamed'}]"
def __repr__(self):
return f"FileElement(media_id={self.media_id}, url={self.url}, path={self.path}, format={self.format})"
# 定义JSON消息元素
class JsonMessage(MessageElement):
def __init__(self, data: str):
self.data = data
def to_dict(self):
return {"type": "json", "data": {"data": self.data}}
def to_plain(self):
return f"[JSON:{self.data}]"
def __repr__(self):
return f"JsonMessage(data={self.data})"
# 定义表情消息元素
class EmojiMessage(MessageElement):
def __init__(self, face_id: str):
self.face_id = face_id
def to_dict(self):
return {"type": "face", "data": {"id": self.face_id}}
def to_plain(self):
return f"[Face:{self.face_id}]"
def __repr__(self):
return f"EmojiMessage(face_id={self.face_id})"
# 定义视频消息元素
class VideoMessage(MediaMessage):
resource_type = "video"
def to_dict(self):
result = super().to_dict()
result["type"] = "video"
return result
def to_plain(self):
return f"[Video:{self.path or self.url or 'unnamed'}]"
def __repr__(self):
return f"VideoMessage(media_id={self.media_id}, url={self.url}, path={self.path}, format={self.format})"
# 定义消息类
class IMMessage:
"""
IM消息类,用于表示一条完整的消息。
包含发送者信息和消息元素列表。
Attributes:
sender: 发送者标识
message_elements: 消息元素列表,可以包含文本、图片、语音等
raw_message: 原始消息数据
content: 消息的纯文本内容
images: 消息中的图片列表
voices: 消息中的语音列表
"""
sender: ChatSender
message_elements: List[MessageElement]
raw_message: Optional[dict]
def __repr__(self):
return f"IMMessage(sender={self.sender}, message_elements={self.message_elements}, raw_message={self.raw_message})"
@property
def content(self) -> str:
"""获取消息的纯文本内容"""
content = ""
for element in self.message_elements:
content += element.to_plain()
if isinstance(element, TextMessage):
content += "\n"
return content.strip()
@property
def images(self) -> List[ImageMessage]:
"""获取消息中的所有图片"""
return [
element
for element in self.message_elements
if isinstance(element, ImageMessage)
]
@property
def voices(self) -> List[VoiceMessage]:
"""获取消息中的所有语音"""
return [
element
for element in self.message_elements
if isinstance(element, VoiceMessage)
]
def __init__(
self,
sender: ChatSender,
message_elements: List[MessageElement],
raw_message: Optional[dict] = None,
):
self.sender = sender
self.message_elements = message_elements
self.raw_message = raw_message
def to_dict(self):
return {
"sender": self.sender,
"message_elements": [
element.to_dict() for element in self.message_elements
],
"plain_text": "".join(
[element.to_plain() for element in self.message_elements]
),
"raw_message": self.raw_message,
}
# backward compatibility
# deprecated
FileElement = FileMessage
ImageElement = ImageMessage
VoiceElement = VoiceMessage
VideoElement = VideoMessage
EmojiElement = EmojiMessage
JsonElement = JsonMessage
FaceElement = EmojiMessage
================================================
FILE: kirara_ai/im/profile.py
================================================
from enum import Enum, auto
from typing import Optional
from pydantic import BaseModel, Field
class Gender(Enum):
MALE = auto()
FEMALE = auto()
UNKNOWN = auto()
OTHER = auto()
class UserProfile(BaseModel):
"""
通用的用户资料结构
"""
user_id: str = Field(..., description="用户唯一标识")
username: Optional[str] = Field(None, description="用户名")
display_name: Optional[str] = Field(None, description="显示名称")
full_name: Optional[str] = Field(None, description="完整名称")
gender: Optional[Gender] = Field(None, description="性别")
age: Optional[int] = Field(None, description="年龄")
avatar_url: Optional[str] = Field(None, description="头像URL")
level: Optional[int] = Field(None, description="用户等级")
language: Optional[str] = Field(None, description="语言")
extra_info: Optional[dict] = Field(None, description="平台特定的额外信息")
def __init__(self, **kwargs):
super().__init__(**kwargs)
================================================
FILE: kirara_ai/im/sender.py
================================================
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Optional
class ChatType(Enum):
C2C = "c2c"
GROUP = "group"
@classmethod
def from_str(cls, value: str) -> "ChatType":
if value == "c2c" or value == "私聊":
return cls.C2C
elif value == "group" or value == "群聊":
return cls.GROUP
raise ValueError(f"Invalid chat type: {value}")
def to_str(self) -> str:
if self == self.C2C:
return "私聊"
elif self == self.GROUP:
return "群聊"
raise ValueError(f"Invalid chat type: {self}")
@dataclass
class ChatSender:
"""聊天发送者信息封装"""
display_name: str
user_id: str
chat_type: ChatType
group_id: Optional[str] = None
raw_metadata: Dict[str, Any] = field(default_factory=dict)
callback = None
@classmethod
def from_group_chat(
cls,
user_id: str,
group_id: str,
display_name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> "ChatSender":
"""创建群聊发送者"""
return cls(
user_id=user_id,
chat_type=ChatType.GROUP,
group_id=group_id,
display_name=display_name,
raw_metadata=metadata or {},
)
@classmethod
def from_c2c_chat(
cls, user_id: str, display_name: str, metadata: Optional[Dict[str, Any]] = None
) -> "ChatSender":
"""创建私聊发送者"""
return cls(
user_id=user_id,
chat_type=ChatType.C2C,
display_name=display_name,
raw_metadata=metadata or {},
)
@classmethod
def get_bot_sender(cls) -> "ChatSender":
"""获取机器人发送者"""
return cls(
user_id="bot",
display_name="bot",
chat_type=ChatType.C2C,
)
def __str__(self) -> str:
if self.chat_type == ChatType.GROUP:
return f"{self.group_id}:{self.user_id}"
else:
return f"c2c:{self.user_id}"
def __eq__(self, other: Any) -> bool:
if isinstance(other, ChatSender):
return self.user_id == other.user_id and \
self.chat_type == other.chat_type and \
self.group_id == other.group_id
return False
def __hash__(self) -> int:
return hash((self.user_id, self.chat_type, self.group_id))
================================================
FILE: kirara_ai/internal.py
================================================
# 定义优雅退出异常
import asyncio
shutdown_event = asyncio.Event()
restart_flag = False
def set_restart_flag():
global restart_flag
restart_flag = True
def get_and_reset_restart_flag():
global restart_flag
flag = restart_flag
restart_flag = False
return flag
================================================
FILE: kirara_ai/ioc/__init__.py
================================================
================================================
FILE: kirara_ai/ioc/container.py
================================================
import contextvars
from typing import Any, Optional, Type, TypeVar, overload
T = TypeVar("T")
class DependencyContainer:
"""
依赖注入容器,提供注册和解析的功能。你可以在此获取一些全局的对象。
基本用法:
```python
# 1. 注册全局对象 - 通常在初始化时使用
container.register(YourObj, your_obj_instance)
# 2. 获取全局对象 - 在你的逻辑代码中使用
your_obj_instance = container.resolve(YourObj)
# 3. 销毁全局对象 - 通常在系统/插件销毁时使用
container.destroy(YourObj)
# 4. 创建作用域容器 - 作用域容器内注册的对象只在作用域内可被访问
# 离开作用域的上下文后无法取到该对象
# 全局容器注册对象
container.register(KiraraObj, kirara_obj)
with container.scoped() as scoped_container:
# 注册作用域对象
scoped_container.register(YourObj, your_obj_instance)
# 获取作用域对象
scoped_container.resolve(YourObj)
# 作用域容器也可以获取到全局容器的对象
container.has(KiraraObj) # 返回 True
# 甚至还能再创建新的作用域容器
with scoped_container.scoped() as another_scoped_container:
another_scoped_container.has(YourObj) # True
# 离开作用域上下文后无法获取到该对象
container.has(YourObj) # 返回 False
```
docs: https://docs.python.org/zh-cn/3.13/library/contextvars.html#module-contextvars
Attributes:
parent (DependencyContainer): 父容器实例,用于支持作用域嵌套
registry (dict): 存储当前容器注册的值或对象实例,格式为{key: value|object}
Methods:
register: 向容器注册一个key-value对
resolve: 从容器解析获取一个值或对象实例
destroy: 从容器中移除一个值或对象实例
scoped: 创建一个新的作用域容器
"""
def __init__(self, parent=None):
self.parent = parent # 父容器,用于支持作用域嵌套
self.registry = {} # 当前容器的注册表
def register(self, key, value):
"""
向容器注册一个值或者实例。
Args:
key: 对象的标识键, 一般为对象的类 (Type) 如 IMManager, LLMManager等,
会根据类型自动查找对应对象实例。
value: 值/对象实例
"""
self.registry[key] = value
@overload
def resolve(self, key: Type[T]) -> T: ...
@overload
def resolve(self, key: Any) -> Any: ...
def resolve(self, key: Type[T] | Any) -> T | Any:
"""
依照{key}从容器解析出一个值或对象实例。
如果{key}在当前容器中不存在,则会递归查找父容器。
Args:
key: 对象的标识键, 一般为对象的类 (Type) 如 IMManager, LLMManager等,
会根据类型自动查找对应对象实例。
Returns:
值/对象实例
Raises:
KeyError: {key}在当前容器和父容器中都不存在时抛出
"""
if key in self.registry:
return self.registry[key]
elif self.parent:
return self.parent.resolve(key)
else:
raise KeyError(f"Dependency {key} not found.")
def has(self, key: Type[T] | Any) -> bool:
"""
检测容器中是否能解析出某个键所对应的值。
Args:
key: 对象的标识键
Returns:
成功返回 True, 失败返回 False
"""
return key in self.registry or (self.parent is not None and self.parent.has(key))
@overload
def destroy(self, key: Type[T], recursive: bool = False) -> None: ...
@overload
def destroy(self, key: Any, recursive: bool = False) -> None: ...
def destroy(self, key: Type[T] | Any, recursive: bool = False) -> None:
"""
从容器中移除一个值或对象实例。支持递归删除父元素。
但是最好不要递归,你可能会删除一些系统对象
Args:
key: 对象的标识键
recursive: 是否递归删除父元素, 默认False。注意这是unsafe方法, 请注意不要删除系统对象。
Raises:
KeyError: {key}在当前容器和父容器中都不存在时抛出
"""
if key in self.registry:
del self.registry[key]
elif self.parent and recursive:
self.parent.destroy(key, recursive)
else:
raise KeyError(f"Cannot destroy dependency {key} which is not found in registry or parent container's registry.")
def scoped(self):
"""创建一个新的作用域容器"""
new_container = ScopedContainer(self)
if DependencyContainer in self.registry:
new_container.registry[DependencyContainer] = new_container
new_container.registry[ScopedContainer] = new_container
return new_container
# 使用 contextvars 实现线程和异步安全的上下文管理
current_container = contextvars.ContextVar[Optional[DependencyContainer]]("current_container", default=None)
class ScopedContainer(DependencyContainer):
def __init__(self, parent):
super().__init__(parent)
def __enter__(self):
# 将当前容器设置为新的作用域容器
self.token = current_container.set(self)
return self
def __exit__(self, exc_type, exc_value, traceback):
# 恢复之前的容器
current_container.reset(self.token)
================================================
FILE: kirara_ai/ioc/inject.py
================================================
from functools import wraps
from inspect import signature
from typing import Any, Callable, Optional, Type
from kirara_ai.ioc.container import DependencyContainer
def get_all_attributes(cls):
if not hasattr(cls, "__annotations__"):
return {}
attributes = dict(cls.__annotations__.items())
# 获取父类的属性和方法
for base in cls.__bases__:
attributes.update(get_all_attributes(base))
return attributes
class Inject:
def __init__(self, container: Optional[DependencyContainer] = None):
self.container = container
def create(self, target: type):
# 注入类
injected_class = self.__call__(target)
# 注入构造函数
return self.inject_function(injected_class)
def __call__(self, target: Any):
# 如果修饰的是一个类
if isinstance(target, type):
return self.inject_class(target)
# 如果修饰的是一个函数
elif callable(target):
return self.inject_function(target)
else:
raise TypeError(
"Inject can only be used on classes, functions."
)
def inject_class(self, cls: Type):
# 遍历类的属性,尝试注入依赖
for name, injecting_type in get_all_attributes(cls).items():
attr = getattr(cls, name) if hasattr(cls, name) else None
setattr(cls, name, self.inject_property(name, cls, injecting_type, attr))
return cls
def inject_function(self, func: Callable):
@wraps(func)
def wrapper(*args, **kwargs):
# 获取函数的参数签名
sig = signature(func)
# 检查是否有 DependencyContainer 对象作为参数传递进来
container_param = self.find_container(args, kwargs)
# 如果有 DependencyContainer 对象,则将其作为 self.container
if container_param:
self.container = container_param
# 遍历参数,注入依赖
bound_args = sig.bind_partial(*args, **kwargs)
bound_args.apply_defaults()
for name, param in sig.parameters.items():
if (
param.annotation != param.empty
and name not in kwargs
and self.container
):
bound_args.arguments[name] = self.container.resolve(
param.annotation
)
# 调用实际的函数
return func(*bound_args.args, **bound_args.kwargs)
return wrapper
def inject_property(self, name, cls, injecting_type, prop: Optional[property]):
# 获取 property 的 fget, fset, fdel
backing_name = f"_{name}_value"
# 定义默认的 getter 方法 (使用实例属性存储值)
def default_fget(_self):
return getattr(_self, backing_name, None)
# 定义默认的 setter 方法 (使用实例属性存储值)
def default_fset(_self, value):
setattr(_self, backing_name, value)
# 定义默认的 deleter 方法 (使用实例属性存储值)
def default_fdel(_self):
if hasattr(_self, backing_name):
delattr(_self, backing_name)
# 如果已有属性,使用其方法,否则使用默认方法
if prop:
fget = prop.fget or default_fget
fset = prop.fset or default_fset
fdel = prop.fdel or default_fdel
else:
fget = default_fget
fset = default_fset
fdel = default_fdel
# 为 property 的 fget 注入依赖
@wraps(fget)
def new_fget(_self):
# 获取 property 的返回值
if self.container and isinstance(injecting_type, type) and self.container.has(injecting_type):
# 如果返回值是一个类型,尝试从 container 中解析
return self.container.resolve(injecting_type)
else:
return default_fget(_self)
# 返回新的 property
return property(new_fget, fset, fdel)
def find_container(self, args, kwargs):
# 检查是否有 DependencyContainer 对象作为参数传递进来
for arg in args:
if isinstance(arg, DependencyContainer):
return arg
for key, value in kwargs.items():
if isinstance(value, DependencyContainer):
return value
return None
================================================
FILE: kirara_ai/llm/adapter.py
================================================
from abc import ABC
from typing import List, Protocol, runtime_checkable
from kirara_ai.config.global_config import ModelConfig
from kirara_ai.llm.format.request import LLMChatRequest
from kirara_ai.llm.format.response import LLMChatResponse
from kirara_ai.llm.format.embedding import LLMEmbeddingRequest, LLMEmbeddingResponse
from kirara_ai.llm.format.rerank import LLMReRankRequest, LLMReRankResponse
from kirara_ai.media.manager import MediaManager
from kirara_ai.tracing.llm_tracer import LLMTracer
@runtime_checkable
class AutoDetectModelsProtocol(Protocol):
async def auto_detect_models(self) -> List[ModelConfig]: ...
@runtime_checkable
class LLMChatProtocol(Protocol):
def chat(self, req: LLMChatRequest) -> LLMChatResponse: ...
@runtime_checkable
class LLMEmbeddingProtocol(Protocol):
def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse: ...
@runtime_checkable
class LLMReRankProtocol(Protocol):
def rerank(self, req: LLMReRankRequest) -> LLMReRankResponse: ...
class LLMBackendAdapter(ABC):
backend_name: str
media_manager: MediaManager
tracer: LLMTracer
================================================
FILE: kirara_ai/llm/format/__init__.py
================================================
from .message import LLMChatImageContent, LLMChatMessage, LLMChatTextContent, LLMToolCallContent, LLMToolResultContent
from .response import LLMChatResponse
from .tool import Function, Tool, ToolCall
__all__ = ["LLMChatMessage", "LLMChatTextContent", "LLMChatImageContent", "LLMToolCallContent", "LLMToolResultContent", "Function", "Tool", "ToolCall", "LLMChatResponse"]
================================================
FILE: kirara_ai/llm/format/embedding.py
================================================
from typing import Literal, Optional
from pydantic import BaseModel
from .message import LLMChatTextContent, LLMChatImageContent
from .response import Usage
FormatType = Literal["base64"]
OutputType = Literal["float", "int8", "uint8", "binary", "ubinary"]
InputType = Literal["string", "query", "document"]
InputUnionType = LLMChatTextContent | LLMChatImageContent
class LLMEmbeddingRequest(BaseModel):
"""
此模型用于规范embedding请求的格式
Tips: 各大模型向量维度以及向量转化函数不同,因此当你用于向量数据库时,请确保存储和检索使用同一个模型,并确保模型向量一致(部分模型支持同一模型设置向量维度)
Note: 注意一下字段为混合字段, 部分字段在部分模型中不起作用, 请参照对应ap文档传递参数。
Attributes:
text (list[str | Image]): 待转化为向量的文本或图片列表
model (str): 使用的embedding模型名
dimensions (Optional[int]): embedding向量的维度
encoding_format (Optional[FormatType]): embedding的编码格式。推荐不设置该字段, 方便直接输入数据库
input_type (Optional[InputType]): 输入类型, 归属于voyage_adapter的独有字段
truncate (Optional[bool]): 是否自动截断超长文本, 以适应llm上下文长度上限。
output_type (Optional[OutputType]): 向量内部应该使用哪种数据类型. 一般默认float
"""
inputs: list[InputUnionType]
model: str
dimension: Optional[int] = None
encoding_format: Optional[FormatType] = None
input_type: Optional[InputType] = None
truncate: Optional[bool] = None
output_type: Optional[OutputType] = None
vector = list[float | int] # 后续可能需要使用numpy库进行精确的数据类型标注, 暂时未处理base64的返回模式
class LLMEmbeddingResponse(BaseModel):
"""
向量维度请使用len(vector)自行计算。
Attributes:
vectors: list[vector]
usage: Optional[Usage] = None
"""
vectors: list[vector]
usage: Optional[Usage] = None
================================================
FILE: kirara_ai/llm/format/message.py
================================================
import json
from typing import Literal, Optional, Union
from pydantic import BaseModel, field_validator, model_validator
from typing_extensions import Self
from .tool import LLMToolResultContent
RoleType = Literal["system", "user", "assistant"]
class LLMChatTextContent(BaseModel):
type: Literal["text"] = "text"
text: str
class LLMChatImageContent(BaseModel):
type: Literal["image"] = "image"
media_id: str
class LLMToolCallContent(BaseModel):
"""
这是模型请求工具的消息内容,
模型强相关内容,如果你 message 或者 memory 内包含了这个内容,请保证调用同一个 model
此部分 role 应该归属于"assistant"
"""
type: Literal["tool_call"] = "tool_call"
# call id,部分模型用此字段区分不同函数的调用,若没有返回则由 Adapter 生成
id: str
name: str
parameters: Optional[dict] = None
@classmethod
@field_validator("parameters", mode="before")
def convert_parameters_to_dict(cls, v: Optional[Union[str, dict]]) -> Optional[dict]:
if isinstance(v, str):
return json.loads(v)
return v
LLMChatContentPartType = Union[LLMChatTextContent, LLMChatImageContent, LLMToolCallContent, LLMToolResultContent]
RoleTypes = Literal["user", "assistant", "system", "tool"]
class LLMChatMessage(BaseModel):
"""
当 role 为 "tool" 时, content 内部只能为 list[LLMToolResultContent]
"""
content: list[LLMChatContentPartType]
role: RoleTypes
@model_validator(mode="after")
def check_content_type(self) -> Self:
# 此装饰器将在 model 实例化后执行,`mode = "after"`
# 用于检查 content 字段的类型是否符合 role 要求
match self.role:
case "user" | "assistant" | "system":
if not all(any(isinstance(element, content_type) for content_type in [LLMChatTextContent, LLMChatImageContent, LLMToolCallContent]) for element in self.content):
raise ValueError(f"content must be a list of LLMChatContentPartType, when role is {self.role}")
case "tool":
if not all(isinstance(element, LLMToolResultContent) for element in self.content):
raise ValueError("content must be a list of LLMToolResultContent, when role is 'tool'")
return self
================================================
FILE: kirara_ai/llm/format/request.py
================================================
from typing import Any, List, Optional
from pydantic import BaseModel
from kirara_ai.llm.format.message import LLMChatMessage
from .tool import Tool
class ResponseFormat(BaseModel):
type: Optional[str] = None
class LLMChatRequest(BaseModel):
"""
Attributes:
tool_choice (Union[dict, Literal["auto", "any", "none"]]):
"
注意由于大模型对于这个接口实现不同,本次暂不实现tool_choice的功能。
tool_choice这个参数告诉llmMessage应该如何选择调用的工具。
"
"""
messages: List[LLMChatMessage] = []
model: Optional[str] = None
frequency_penalty: Optional[int] = None
max_tokens: Optional[int] = None
presence_penalty: Optional[int] = None
response_format: Optional[ResponseFormat] = None
stop: Optional[Any] = None
stream: Optional[bool] = None
stream_options: Optional[Any] = None
temperature: Optional[int] = None
top_p: Optional[int] = None
# 规范tool传递
tools: Optional[list[Tool]] = None
# tool_choice各家目前标准不尽相同,暂不向用户提供更改这个值的选项
tool_choice: Optional[Any] = None
logprobs: Optional[bool] = None
top_logprobs: Optional[Any] = None
================================================
FILE: kirara_ai/llm/format/rerank.py
================================================
from typing import Optional
from typing_extensions import Self
from pydantic import BaseModel, model_validator
from .response import Usage
class LLMReRankRequest(BaseModel):
"""
ReRanker: 重排器是一个重要的处理方案, 通常见于 EsSearch 的优化方案中。
本接口是适用于 LLM 的重排器的请求模型一般与嵌入式式模型组合使用提高向量搜索准确率。
传入一系列原始文档和一个查询语句,返回器相似度数值。
Attributes:
query: 原始查询语句
documents: 文档列表, 包含文档的文本内容。每个文档转化为一个 string 类型传递。
model: 重排模型的名称。为保证准确性,本实现将禁止自动选择模型。
top_k: 返回最相似的 {top_k} 个文档。如果没有指定,将返回所有文档的重排序结果。
Tips: 如果你决定不返回原始文档,那么不要设置这个选项。会丢失文本与相似度的关联。
return_documents: 是否返回原始文档内容。
truncation: 文档和查询语句是否允许被截断以适应模型最大上下文。
sort: 是否按照结果的相似度得分进行排序? 默认不进行
Tips: 当return_documents为False时,若sort为True,则抛出异常。
"""
query: str
documents: list[str]
model: str
top_k: Optional[int] = None
return_documents: Optional[bool] = None
truncation: Optional[bool] = None
sort: Optional[bool] = False
@model_validator(mode="after") # mode 为 before 时其用法与after完全不同,注意看官网文档
# 这里不用after是为了等pydantic赋值默认值后检查
def check(self) -> Self:
if self.sort and not self.return_documents:
raise ValueError("Cannot sort server responses when return_documents is False.")
return self
class ReRankerContent(BaseModel):
"""
ReRanker 的内容模型。
Attributes:
document: 原始文档内容。
score: 文档的相似度分数。
"""
document: Optional[str] = None
score: float
class LLMReRankResponse(BaseModel):
"""
ReRanker 的返回模型。
Attributes:
contents (list[ReRankerContent]): 返回的排序信息, 如果启用排序,默认降序排列。 Note: 当且仅当return_documents为True时才允许启用排序。
usage (Usage): token 使用情况, 一个pydantic.BaseModel的子类。
sort (bool): 是否按照结果的相似度排序?将其设置为字段方便后续接口检查是否经过排序(方便debug)。其应该由request的sort字段赋值。
"""
contents: list[ReRankerContent]
usage: Usage
sort: bool
@model_validator(mode="after") # 当mode为after时,其发生在class实例化完成后,所以其为实例方法
def sort_content(self) -> Self:
if self.sort:
self.contents = sorted(self.contents, key= lambda x: x.score, reverse=True)
return self
================================================
FILE: kirara_ai/llm/format/response.py
================================================
from typing import List, Optional
from pydantic import BaseModel
from kirara_ai.llm.format.message import LLMChatMessage
from kirara_ai.llm.format.tool import ToolCall
class Message(LLMChatMessage):
tool_calls: Optional[List[ToolCall]] = None
finish_reason: Optional[str] = None
class Usage(BaseModel):
completion_tokens: Optional[int] = None
prompt_tokens: Optional[int] = None
total_tokens: Optional[int] = None
cached_tokens: Optional[int] = None
class LLMChatResponse(BaseModel):
model: Optional[str] = None
usage: Optional[Usage] = None
message: Message
================================================
FILE: kirara_ai/llm/format/tool.py
================================================
import json
from typing import Any, Callable, Coroutine, Generic, List, Literal, Optional, TypeVar, Union
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
class TextContent(BaseModel):
type: Literal["text"] = "text"
text: str
class MediaContent(BaseModel):
type: Literal["media"] = "media"
media_id: str
mime_type: str
data: bytes
ToolResponseTypes = List[Union[TextContent, MediaContent]]
class LLMToolResultContent(BaseModel):
"""
这是工具回应的消息内容,
模型强相关内容,如果你 message 或者 memory 内包含了这个内容,请保证调用同一个 model
此部分 role 应该对应 "tool"
"""
type: Literal["tool_result"] = "tool_result"
# call id,对应 LLMToolCallContent 的 id
id: str
name: str
# 各家工具要求返回的content格式不同. 等待后续规范化。
content: ToolResponseTypes
isError: bool = False
class Function(BaseModel):
# 工具名称
name: str
# 这个字段类似于 python 的关键字参数,你可以直接使用`**arguments`
arguments: Optional[dict] = None
@field_validator("arguments", mode="before")
@classmethod
# pydantic 官网建议将 @classmethod 放在下面。因为python装饰器执行顺序是由下到上。
def convert_arguments(cls, v: Optional[Union[str, dict]]) -> Optional[dict]:
return json.loads(v) if isinstance(v, str) else v
class ToolCall(BaseModel):
# call id,对应 LLMToolCallContent 的 id
id: str
# type这个字段目前不知道有什么用
type: Optional[str] = None
function: Function
T = TypeVar('T', bound=Callable)
ToolInvokeFunc = Callable[[ToolCall], Coroutine[Any, Any, "LLMToolResultContent"]]
class CallableWrapper(Generic[T]):
"""包装可调用对象的类,在深拷贝时返回None"""
def __init__(self, func: T):
self.func = func
def __call__(self, *args, **kwargs) -> Coroutine[Any, Any, "LLMToolResultContent"]:
return self.func(*args, **kwargs)
def __deepcopy__(self, memo):
# 深拷贝时保持原始引用而不是尝试复制函数
return self
class ToolInputSchema(BaseModel):
"""
工具输入参数的格式,遵循 JSON Schema 的规范
Attributes:
type (Literal["object"]): 参数的类型
properties (dict): 工具属性,参考 openai api 的规范
required (list[str]): 必填参数的名称列表
additionalProperties (Optional[bool]): 是否允许额外的键值对
"""
type: Literal["object"] = "object"
properties: dict
required: list[str]
additionalProperties: Optional[bool] = False
class Tool(BaseModel):
"""
传递给 LLM 的工具信息
Attributes:
type (Optional[Literal["function"]]): 工具的类型
name (str): 工具的名称
description (str): 工具的描述
parameters (ToolInputSchema): 工具的参数格式
strict (Optional[bool]): 是否严格调用, openai api专属
invokeFunc (Optional[Callable]): 工具对应的执行函数,仅在调用时使用,不参与序列化
"""
type: Optional[Literal["function"]] = "function"
name: str
description: str
parameters: Union[ToolInputSchema, dict]
strict: Optional
gitextract_9czk0gzq/
├── .cursor/
│ └── rules/
│ └── create-workflow.mdc
├── .dockerignore
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── feature-request.md
│ ├── dependabot.yml
│ ├── quickstarts/
│ │ └── windows/
│ │ └── scripts/
│ │ └── 启动.cmd
│ └── workflows/
│ ├── docker-latest.yml
│ ├── docker-tag.yml
│ ├── pr_review.yml
│ ├── project_check.yml
│ ├── quickstart-windows.yml
│ ├── run-tests.yml
│ └── stale.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── README.md
├── alembic.ini
├── config.yaml.example
├── data/
│ ├── .gitkeep
│ ├── dispatch_rules/
│ │ └── rules.yaml
│ ├── media/
│ │ └── .gitignore
│ ├── memory/
│ │ └── .gitignore
│ ├── web/
│ │ └── .gitkeep
│ └── workflows/
│ ├── .gitkeep
│ └── chat/
│ ├── dsr_thinking.yaml
│ ├── memory_store.yaml
│ ├── normal_multimodal.yaml
│ └── talk_break.yaml
├── docker/
│ └── start.sh
├── kirara_ai/
│ ├── __init__.py
│ ├── __main__.py
│ ├── alembic/
│ │ ├── README
│ │ ├── env.py
│ │ ├── script.py.mako
│ │ └── versions/
│ │ └── 4a364dbb8dab_initial_migration.py
│ ├── config/
│ │ ├── __init__.py
│ │ ├── config_loader.py
│ │ └── global_config.py
│ ├── database/
│ │ ├── __init__.py
│ │ └── manager.py
│ ├── entry.py
│ ├── events/
│ │ ├── __init__.py
│ │ ├── application.py
│ │ ├── event_bus.py
│ │ ├── im.py
│ │ ├── listen.py
│ │ ├── llm.py
│ │ ├── plugin.py
│ │ ├── tracing/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── llm.py
│ │ └── workflow.py
│ ├── im/
│ │ ├── __init__.py
│ │ ├── adapter.py
│ │ ├── im_registry.py
│ │ ├── manager.py
│ │ ├── message.py
│ │ ├── profile.py
│ │ └── sender.py
│ ├── internal.py
│ ├── ioc/
│ │ ├── __init__.py
│ │ ├── container.py
│ │ └── inject.py
│ ├── llm/
│ │ ├── adapter.py
│ │ ├── format/
│ │ │ ├── __init__.py
│ │ │ ├── embedding.py
│ │ │ ├── message.py
│ │ │ ├── request.py
│ │ │ ├── rerank.py
│ │ │ ├── response.py
│ │ │ └── tool.py
│ │ ├── llm_manager.py
│ │ ├── llm_registry.py
│ │ └── model_types.py
│ ├── logger.py
│ ├── mcp_module/
│ │ ├── __init__.py
│ │ ├── manager.py
│ │ ├── models.py
│ │ └── server.py
│ ├── media/
│ │ ├── __init__.py
│ │ ├── carrier/
│ │ │ ├── __init__.py
│ │ │ ├── provider.py
│ │ │ ├── registry.py
│ │ │ └── service.py
│ │ ├── manager.py
│ │ ├── media_object.py
│ │ ├── metadata.py
│ │ ├── types/
│ │ │ ├── __init__.py
│ │ │ └── media_type.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── mime.py
│ ├── memory/
│ │ ├── composes/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── builtin_composes.py
│ │ │ ├── composer_strategy.py
│ │ │ ├── decomposer_strategy.py
│ │ │ └── xml_helper.py
│ │ ├── entry.py
│ │ ├── memory_manager.py
│ │ ├── persistences/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── codecs.py
│ │ │ ├── file_persistence.py
│ │ │ └── redis_persistence.py
│ │ ├── registry.py
│ │ └── scopes/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── builtin_scopes.py
│ ├── plugin_manager/
│ │ ├── models.py
│ │ ├── plugin.py
│ │ ├── plugin_event_bus.py
│ │ ├── plugin_loader.py
│ │ └── utils.py
│ ├── plugins/
│ │ ├── .gitkeep
│ │ ├── bundled_frpc/
│ │ │ ├── __init__.py
│ │ │ ├── frpc_manager.py
│ │ │ ├── models.py
│ │ │ └── routes.py
│ │ ├── im_http_legacy_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── setup.py
│ │ │ └── tests/
│ │ │ └── api_test.py
│ │ ├── im_qqbot_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── setup.py
│ │ │ └── utils.py
│ │ ├── im_telegram_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ └── setup.py
│ │ ├── im_wecom_adapter/
│ │ │ ├── __init__.py
│ │ │ ├── adapter.py
│ │ │ ├── delegates.py
│ │ │ └── setup.py
│ │ └── llm_preset_adapters/
│ │ ├── __init__.py
│ │ ├── alibabacloud_adapter.py
│ │ ├── claude_adapter.py
│ │ ├── deepseek_adapter.py
│ │ ├── gemini_adapter.py
│ │ ├── minimax_adapter.py
│ │ ├── mistral_adapter.py
│ │ ├── moonshot_adapter.py
│ │ ├── ollama_adapter.py
│ │ ├── openai_adapter.py
│ │ ├── openrouter_adapter.py
│ │ ├── setup.py
│ │ ├── siliconflow_adapter.py
│ │ ├── tencentcloud_adapter.py
│ │ ├── tests/
│ │ │ └── test_utils.py
│ │ ├── utils.py
│ │ ├── volcengine_adapter.py
│ │ └── voyage_adapter.py
│ ├── system/
│ │ ├── __init__.py
│ │ └── updater.py
│ ├── tracing/
│ │ ├── __init__.py
│ │ ├── core.py
│ │ ├── decorator.py
│ │ ├── llm_tracer.py
│ │ ├── manager.py
│ │ └── models.py
│ ├── web/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── block/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── diagnostics/
│ │ │ │ │ ├── base_diagnostic.py
│ │ │ │ │ ├── import_check.py
│ │ │ │ │ ├── jedi_syntax_check.py
│ │ │ │ │ ├── mandatory_function.py
│ │ │ │ │ └── pyflakes_check.py
│ │ │ │ ├── models.py
│ │ │ │ ├── python_lsp.py
│ │ │ │ └── routes.py
│ │ │ ├── dispatch/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── im/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── llm/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── mcp/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── media/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── plugin/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ └── routes.py
│ │ │ ├── system/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py
│ │ │ │ ├── routes.py
│ │ │ │ └── utils.py
│ │ │ ├── tracing/
│ │ │ │ ├── __init__.py
│ │ │ │ └── routes.py
│ │ │ └── workflow/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ └── routes.py
│ │ ├── app.py
│ │ ├── auth/
│ │ │ ├── middleware.py
│ │ │ ├── models.py
│ │ │ ├── routes.py
│ │ │ ├── services.py
│ │ │ └── utils.py
│ │ └── utils.py
│ └── workflow/
│ ├── core/
│ │ ├── __init__.py
│ │ ├── block/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── input_output.py
│ │ │ ├── param.py
│ │ │ ├── registry.py
│ │ │ ├── schema.py
│ │ │ └── type_system.py
│ │ ├── dispatch/
│ │ │ ├── __init__.py
│ │ │ ├── dispatcher.py
│ │ │ ├── exceptions.py
│ │ │ ├── models/
│ │ │ │ └── dispatch_rules.py
│ │ │ ├── registry.py
│ │ │ └── rules/
│ │ │ ├── base.py
│ │ │ ├── message_rules.py
│ │ │ ├── sender_rules.py
│ │ │ └── system_rules.py
│ │ ├── execution/
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ └── executor.py
│ │ └── workflow/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── builder.py
│ │ └── registry.py
│ ├── implementations/
│ │ ├── __init__.py
│ │ ├── blocks/
│ │ │ ├── __init__.py
│ │ │ ├── game/
│ │ │ │ ├── dice.py
│ │ │ │ └── gacha.py
│ │ │ ├── im/
│ │ │ │ ├── basic.py
│ │ │ │ ├── messages.py
│ │ │ │ ├── states.py
│ │ │ │ └── user_profile.py
│ │ │ ├── llm/
│ │ │ │ ├── basic.py
│ │ │ │ ├── chat.py
│ │ │ │ └── image.py
│ │ │ ├── mcp/
│ │ │ │ ├── __init__.py
│ │ │ │ └── tool.py
│ │ │ ├── memory/
│ │ │ │ ├── chat_memory.py
│ │ │ │ └── clear_memory.py
│ │ │ ├── system/
│ │ │ │ ├── basic.py
│ │ │ │ └── help.py
│ │ │ ├── system_blocks.py
│ │ │ └── variables/
│ │ │ └── variable_blocks.py
│ │ ├── factories/
│ │ │ ├── __init__.py
│ │ │ ├── default_factory.py
│ │ │ ├── game_factory.py
│ │ │ └── system_factory.py
│ │ └── workflows/
│ │ ├── __init__.py
│ │ └── system_workflows.py
│ └── utils/
│ └── __init__.py
├── pyproject.toml
├── pytest.ini
└── tests/
├── __init__.py
├── llm_adapters/
│ ├── __init__.py
│ ├── conftest.py
│ ├── mock_app/
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── gemini.py
│ │ ├── models/
│ │ │ ├── gemini.py
│ │ │ └── openai.py
│ │ ├── ollama.py
│ │ ├── openai.py
│ │ └── voyage.py
│ ├── test_gemini_adapter.py
│ ├── test_ollama_adapter.py
│ ├── test_openai_adapter.py
│ └── test_voyage_adapter.py
├── memory/
│ ├── __init__.py
│ ├── test_composer_decomposer.py
│ ├── test_composer_strategy.py
│ ├── test_decomposer_strategy.py
│ ├── test_memory_manager.py
│ ├── test_persistence.py
│ └── test_scope.py
├── resources/
│ └── test_image.txt
├── system_blocks/
│ ├── __init__.py
│ ├── game/
│ │ ├── __init__.py
│ │ ├── test_dice.py
│ │ └── test_gacha.py
│ ├── im/
│ │ ├── __init__.py
│ │ ├── test_messages.py
│ │ └── test_states.py
│ ├── llm/
│ │ ├── __init__.py
│ │ ├── test_basic.py
│ │ ├── test_chat.py
│ │ └── test_image.py
│ ├── memory/
│ │ ├── __init__.py
│ │ ├── test_chat_memory.py
│ │ └── test_clear_memory.py
│ └── system/
│ ├── __init__.py
│ ├── test_basic.py
│ └── test_help.py
├── test_config_loader.py
├── test_game_blocks.py
├── test_mcp_server.py
├── test_media.py
├── test_media_element.py
├── test_system_blocks.py
├── test_workflow_builder.py
├── test_workflow_factories.py
├── tracing/
│ ├── __init__.py
│ ├── test_base.py
│ ├── test_core.py
│ ├── test_decorator.py
│ ├── test_llm_tracer.py
│ ├── test_manager.py
│ └── test_models.py
├── utils/
│ ├── auth_test_utils.py
│ └── test_block_registry.py
├── web/
│ ├── api/
│ │ ├── im/
│ │ │ └── test_im.py
│ │ ├── llm/
│ │ │ └── test_llm.py
│ │ ├── media/
│ │ │ └── test_media.py
│ │ ├── plugin/
│ │ │ └── test_plugin.py
│ │ ├── system/
│ │ │ └── test_system.py
│ │ └── workflow/
│ │ └── test_workflow.py
│ └── auth/
│ └── test_auth.py
└── workflow_executor/
├── test_block.py
├── test_executor.py
├── test_input_output.py
└── test_workflow_basic.py
SYMBOL INDEX (2024 symbols across 230 files)
FILE: kirara_ai/__main__.py
function main (line 10) | def main():
FILE: kirara_ai/alembic/env.py
function run_migrations_offline (line 30) | def run_migrations_offline() -> None:
function run_migrations_online (line 54) | def run_migrations_online() -> None:
FILE: kirara_ai/alembic/versions/4a364dbb8dab_initial_migration.py
function upgrade (line 20) | def upgrade() -> None:
function downgrade (line 51) | def downgrade() -> None:
FILE: kirara_ai/config/config_loader.py
class ConfigLoader (line 17) | class ConfigLoader:
method load_config (line 25) | def load_config(config_path: str, config_class: Type[T]) -> T:
method save_config (line 42) | def save_config(config_path: str, config_object: BaseModel):
method save_config_with_backup (line 52) | def save_config_with_backup(config_path: str, config_object: BaseModel):
function pydantic_validation_wrapper (line 65) | def pydantic_validation_wrapper(func):
class ConfigJsonSchema (line 86) | class ConfigJsonSchema(GenerateJsonSchema):
method sort (line 87) | def sort(
FILE: kirara_ai/config/global_config.py
class IMConfig (line 8) | class IMConfig(BaseModel):
class ModelConfig (line 17) | class ModelConfig(BaseModel):
class LLMBackendConfig (line 26) | class LLMBackendConfig(BaseModel):
method migrate_models_format (line 39) | def migrate_models_format(cls, data: Dict[str, Any]) -> Dict[str, Any]:
class LLMConfig (line 61) | class LLMConfig(BaseModel):
class MCPServerConfig (line 66) | class MCPServerConfig(BaseModel):
class MCPConfig (line 80) | class MCPConfig(BaseModel):
class DefaultConfig (line 85) | class DefaultConfig(BaseModel):
class MemoryPersistenceConfig (line 91) | class MemoryPersistenceConfig(BaseModel):
class MemoryConfig (line 102) | class MemoryConfig(BaseModel):
class WebConfig (line 108) | class WebConfig(BaseModel):
class PluginConfig (line 117) | class PluginConfig(BaseModel):
class UpdateConfig (line 127) | class UpdateConfig(BaseModel):
class FrpcConfig (line 132) | class FrpcConfig(BaseModel):
class SystemConfig (line 142) | class SystemConfig(BaseModel):
class TracingConfig (line 148) | class TracingConfig(BaseModel):
class MediaConfig (line 153) | class MediaConfig(BaseModel):
class GlobalConfig (line 159) | class GlobalConfig(BaseModel):
FILE: kirara_ai/database/manager.py
class DatabaseManager (line 20) | class DatabaseManager:
method __init__ (line 23) | def __init__(self, container: DependencyContainer, database_url: Optio...
method initialize (line 32) | def initialize(self):
method _run_migrations (line 53) | def _run_migrations(self):
method get_session (line 89) | def get_session(self) -> Session:
method shutdown (line 96) | def shutdown(self):
FILE: kirara_ai/entry.py
function check_update (line 40) | async def check_update():
function _signal_handler (line 52) | def _signal_handler(*args):
function init_container (line 67) | def init_container() -> DependencyContainer:
function init_memory_system (line 73) | def init_memory_system(container: DependencyContainer):
function init_media_carrier (line 90) | def init_media_carrier(container: DependencyContainer):
function init_tracing_system (line 96) | def init_tracing_system(container: DependencyContainer):
function init_application (line 115) | def init_application() -> DependencyContainer:
function run_application (line 229) | def run_application(container: DependencyContainer):
FILE: kirara_ai/events/application.py
class ApplicationStarted (line 2) | class ApplicationStarted:
method __repr__ (line 3) | def __repr__(self):
class ApplicationStopping (line 6) | class ApplicationStopping:
method __repr__ (line 7) | def __repr__(self):
FILE: kirara_ai/events/event_bus.py
class EventBus (line 7) | class EventBus:
method __init__ (line 8) | def __init__(self):
method register (line 11) | def register(self, event_type: Type, listener: Callable):
method unregister (line 16) | def unregister(self, event_type: Type, listener: Callable):
method post (line 20) | def post(self, event):
FILE: kirara_ai/events/im.py
class IMEvent (line 7) | class IMEvent:
method __init__ (line 8) | def __init__(self, im: "IMAdapter"):
method __repr__ (line 11) | def __repr__(self):
class IMAdapterStarted (line 14) | class IMAdapterStarted(IMEvent):
class IMAdapterStopped (line 17) | class IMAdapterStopped(IMEvent):
FILE: kirara_ai/events/listen.py
function listen (line 7) | def listen(event_bus: EventBus):
FILE: kirara_ai/events/llm.py
class LLMAdapterEvent (line 4) | class LLMAdapterEvent:
method __init__ (line 5) | def __init__(self, adapter: LLMBackendAdapter, backend_name: str):
method __repr__ (line 9) | def __repr__(self):
class LLMAdapterLoaded (line 12) | class LLMAdapterLoaded(LLMAdapterEvent):
class LLMAdapterUnloaded (line 15) | class LLMAdapterUnloaded(LLMAdapterEvent):
FILE: kirara_ai/events/plugin.py
class PluginEvent (line 5) | class PluginEvent:
method __init__ (line 6) | def __init__(self, plugin: Plugin):
method __repr__ (line 9) | def __repr__(self):
class PluginStarted (line 12) | class PluginStarted(PluginEvent):
class PluginStopped (line 15) | class PluginStopped(PluginEvent):
class PluginLoaded (line 18) | class PluginLoaded(PluginEvent):
FILE: kirara_ai/events/tracing/base.py
class TraceEvent (line 6) | class TraceEvent(abc.ABC):
method __init__ (line 9) | def __init__(self, trace_id: str):
method __repr__ (line 13) | def __repr__(self):
class TraceStartEvent (line 17) | class TraceStartEvent(TraceEvent):
class TraceCompleteEvent (line 21) | class TraceCompleteEvent(TraceEvent):
class TraceFailEvent (line 25) | class TraceFailEvent(TraceEvent):
FILE: kirara_ai/events/tracing/llm.py
class LLMTraceEvent (line 10) | class LLMTraceEvent(TraceEvent):
method __init__ (line 13) | def __init__(self,
method __repr__ (line 21) | def __repr__(self):
class LLMRequestStartEvent (line 25) | class LLMRequestStartEvent(LLMTraceEvent, TraceStartEvent):
method __init__ (line 28) | def __init__(self,
class LLMRequestCompleteEvent (line 38) | class LLMRequestCompleteEvent(LLMTraceEvent, TraceCompleteEvent):
method __init__ (line 41) | def __init__(self,
class LLMRequestFailEvent (line 55) | class LLMRequestFailEvent(LLMTraceEvent, TraceFailEvent):
method __init__ (line 58) | def __init__(self,
FILE: kirara_ai/events/workflow.py
class WorkflowExecutionBegin (line 7) | class WorkflowExecutionBegin:
method __init__ (line 8) | def __init__(self, workflow: Workflow, executor: WorkflowExecutor):
method __repr__ (line 12) | def __repr__(self):
class WorkflowExecutionEnd (line 16) | class WorkflowExecutionEnd:
method __init__ (line 17) | def __init__(self, workflow: Workflow, executor: WorkflowExecutor, res...
FILE: kirara_ai/im/adapter.py
class BotStatus (line 14) | class BotStatus(BaseModel):
class EditStateAdapter (line 23) | class EditStateAdapter(Protocol):
method set_chat_editing_state (line 28) | async def set_chat_editing_state(
class UserProfileAdapter (line 39) | class UserProfileAdapter(Protocol):
method query_user_profile (line 44) | async def query_user_profile(self, chat_sender: ChatSender) -> UserPro...
class BotProfileAdapter (line 52) | class BotProfileAdapter(Protocol):
method get_bot_profile (line 57) | async def get_bot_profile(self) -> Optional[UserProfile]:
class IMAdapter (line 63) | class IMAdapter(ABC):
method convert_to_message (line 73) | async def convert_to_message(self, raw_message: Any) -> IMMessage:
method send_message (line 81) | async def send_message(self, message: IMMessage, recipient: Any):
method start (line 89) | async def start(self):
method stop (line 93) | async def stop(self):
FILE: kirara_ai/im/im_registry.py
class IMAdapterInfo (line 8) | class IMAdapterInfo(BaseModel):
class IMRegistry (line 18) | class IMRegistry:
method register (line 25) | def register(
method unregister (line 49) | def unregister(self, name: str):
method get (line 56) | def get(self, name: str) -> Type[IMAdapter]:
method get_config_class (line 67) | def get_config_class(self, name: str) -> Type[BaseModel]:
method get_all_adapters (line 79) | def get_all_adapters(self) -> Dict[str, IMAdapterInfo]:
FILE: kirara_ai/im/manager.py
class IMManager (line 19) | class IMManager:
method __init__ (line 33) | def __init__(
method get_adapter_type (line 46) | def get_adapter_type(self, name: str) -> str:
method has_adapter (line 54) | def has_adapter(self, name: str) -> bool:
method get_adapter_config (line 62) | def get_adapter_config(self, name: str) -> IMConfig:
method update_adapter_config (line 73) | def update_adapter_config(self, name: str, config: BaseModel):
method delete_adapter (line 81) | def delete_adapter(self, name: str):
method start_adapters (line 90) | def start_adapters(self, loop=None):
method stop_adapters (line 126) | def stop_adapters(self, loop=None):
method get_adapters (line 137) | def get_adapters(self) -> Dict[str, IMAdapter]:
method get_adapter (line 144) | def get_adapter(self, key: str) -> IMAdapter:
method _start_adapter (line 152) | async def _start_adapter(self, key: str, adapter: IMAdapter):
method _stop_adapter (line 159) | async def _stop_adapter(self, key: str, adapter: IMAdapter):
method stop_adapter (line 166) | def stop_adapter(self, adapter_id: str, loop: asyncio.AbstractEventLoop):
method start_adapter (line 172) | def start_adapter(self, adapter_id: str, loop: asyncio.AbstractEventLo...
method is_adapter_running (line 180) | def is_adapter_running(self, key: str) -> bool:
method create_adapter (line 189) | def create_adapter(
FILE: kirara_ai/im/message.py
class MessageElement (line 16) | class MessageElement(ABC):
method to_dict (line 18) | def to_dict(self):
method to_plain (line 22) | def to_plain(self) -> str:
class TextMessage (line 27) | class TextMessage(MessageElement):
method __init__ (line 28) | def __init__(self, text: str):
method to_dict (line 31) | def to_dict(self):
method to_plain (line 34) | def to_plain(self):
method __repr__ (line 37) | def __repr__(self):
class MediaMessage (line 42) | class MediaMessage(MessageElement):
method __init__ (line 48) | def __init__(
method _register_media (line 102) | async def _register_media(self) -> None:
method get_url (line 126) | async def get_url(self) -> str:
method get_path (line 144) | async def get_path(self) -> str:
method get_data (line 162) | async def get_data(self) -> bytes:
method get_base64_url (line 180) | async def get_base64_url(self) -> str:
method get_description (line 195) | def get_description(self) -> str:
method to_dict (line 204) | def to_dict(self) -> Dict[str, Any]:
class VoiceMessage (line 223) | class VoiceMessage(MediaMessage):
method to_dict (line 226) | def to_dict(self):
method to_plain (line 231) | def to_plain(self):
class ImageMessage (line 236) | class ImageMessage(MediaMessage):
method to_dict (line 239) | def to_dict(self):
method to_plain (line 244) | def to_plain(self):
method __repr__ (line 247) | def __repr__(self):
class AtElement (line 252) | class AtElement(MessageElement):
method __init__ (line 253) | def __init__(self, user_id: str, nickname: str = ""):
method to_dict (line 257) | def to_dict(self):
method to_plain (line 260) | def to_plain(self):
method __repr__ (line 263) | def __repr__(self):
class MentionElement (line 267) | class MentionElement(MessageElement):
method __init__ (line 268) | def __init__(self, target: ChatSender):
method to_dict (line 271) | def to_dict(self):
method to_plain (line 274) | def to_plain(self):
method __repr__ (line 277) | def __repr__(self):
class ReplyElement (line 281) | class ReplyElement(MessageElement):
method __init__ (line 283) | def __init__(self, message_id: str):
method to_dict (line 286) | def to_dict(self):
method to_plain (line 290) | def to_plain(self):
method __repr__ (line 293) | def __repr__(self):
class FileMessage (line 298) | class FileMessage(MediaMessage):
method to_dict (line 301) | def to_dict(self):
method to_plain (line 306) | def to_plain(self):
method __repr__ (line 309) | def __repr__(self):
class JsonMessage (line 314) | class JsonMessage(MessageElement):
method __init__ (line 316) | def __init__(self, data: str):
method to_dict (line 319) | def to_dict(self):
method to_plain (line 322) | def to_plain(self):
method __repr__ (line 325) | def __repr__(self):
class EmojiMessage (line 330) | class EmojiMessage(MessageElement):
method __init__ (line 332) | def __init__(self, face_id: str):
method to_dict (line 335) | def to_dict(self):
method to_plain (line 339) | def to_plain(self):
method __repr__ (line 342) | def __repr__(self):
class VideoMessage (line 347) | class VideoMessage(MediaMessage):
method to_dict (line 350) | def to_dict(self):
method to_plain (line 355) | def to_plain(self):
method __repr__ (line 358) | def __repr__(self):
class IMMessage (line 363) | class IMMessage:
method __repr__ (line 381) | def __repr__(self):
method content (line 385) | def content(self) -> str:
method images (line 395) | def images(self) -> List[ImageMessage]:
method voices (line 404) | def voices(self) -> List[VoiceMessage]:
method __init__ (line 412) | def __init__(
method to_dict (line 422) | def to_dict(self):
FILE: kirara_ai/im/profile.py
class Gender (line 7) | class Gender(Enum):
class UserProfile (line 14) | class UserProfile(BaseModel):
method __init__ (line 30) | def __init__(self, **kwargs):
FILE: kirara_ai/im/sender.py
class ChatType (line 6) | class ChatType(Enum):
method from_str (line 11) | def from_str(cls, value: str) -> "ChatType":
method to_str (line 18) | def to_str(self) -> str:
class ChatSender (line 26) | class ChatSender:
method from_group_chat (line 37) | def from_group_chat(
method from_c2c_chat (line 54) | def from_c2c_chat(
method get_bot_sender (line 66) | def get_bot_sender(cls) -> "ChatSender":
method __str__ (line 74) | def __str__(self) -> str:
method __eq__ (line 80) | def __eq__(self, other: Any) -> bool:
method __hash__ (line 87) | def __hash__(self) -> int:
FILE: kirara_ai/internal.py
function set_restart_flag (line 9) | def set_restart_flag():
function get_and_reset_restart_flag (line 13) | def get_and_reset_restart_flag():
FILE: kirara_ai/ioc/container.py
class DependencyContainer (line 6) | class DependencyContainer:
method __init__ (line 52) | def __init__(self, parent=None):
method register (line 56) | def register(self, key, value):
method resolve (line 69) | def resolve(self, key: Type[T]) -> T: ...
method resolve (line 72) | def resolve(self, key: Any) -> Any: ...
method resolve (line 74) | def resolve(self, key: Type[T] | Any) -> T | Any:
method has (line 98) | def has(self, key: Type[T] | Any) -> bool:
method destroy (line 109) | def destroy(self, key: Type[T], recursive: bool = False) -> None: ...
method destroy (line 112) | def destroy(self, key: Any, recursive: bool = False) -> None: ...
method destroy (line 114) | def destroy(self, key: Type[T] | Any, recursive: bool = False) -> None:
method scoped (line 133) | def scoped(self):
class ScopedContainer (line 146) | class ScopedContainer(DependencyContainer):
method __init__ (line 147) | def __init__(self, parent):
method __enter__ (line 150) | def __enter__(self):
method __exit__ (line 155) | def __exit__(self, exc_type, exc_value, traceback):
FILE: kirara_ai/ioc/inject.py
function get_all_attributes (line 8) | def get_all_attributes(cls):
class Inject (line 19) | class Inject:
method __init__ (line 20) | def __init__(self, container: Optional[DependencyContainer] = None):
method create (line 23) | def create(self, target: type):
method __call__ (line 29) | def __call__(self, target: Any):
method inject_class (line 41) | def inject_class(self, cls: Type):
method inject_function (line 48) | def inject_function(self, func: Callable):
method inject_property (line 77) | def inject_property(self, name, cls, injecting_type, prop: Optional[pr...
method find_container (line 117) | def find_container(self, args, kwargs):
FILE: kirara_ai/llm/adapter.py
class AutoDetectModelsProtocol (line 14) | class AutoDetectModelsProtocol(Protocol):
method auto_detect_models (line 15) | async def auto_detect_models(self) -> List[ModelConfig]: ...
class LLMChatProtocol (line 18) | class LLMChatProtocol(Protocol):
method chat (line 19) | def chat(self, req: LLMChatRequest) -> LLMChatResponse: ...
class LLMEmbeddingProtocol (line 22) | class LLMEmbeddingProtocol(Protocol):
method embed (line 23) | def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse: ...
class LLMReRankProtocol (line 26) | class LLMReRankProtocol(Protocol):
method rerank (line 27) | def rerank(self, req: LLMReRankRequest) -> LLMReRankResponse: ...
class LLMBackendAdapter (line 29) | class LLMBackendAdapter(ABC):
FILE: kirara_ai/llm/format/embedding.py
class LLMEmbeddingRequest (line 12) | class LLMEmbeddingRequest(BaseModel):
class LLMEmbeddingResponse (line 36) | class LLMEmbeddingResponse(BaseModel):
FILE: kirara_ai/llm/format/message.py
class LLMChatTextContent (line 11) | class LLMChatTextContent(BaseModel):
class LLMChatImageContent (line 15) | class LLMChatImageContent(BaseModel):
class LLMToolCallContent (line 19) | class LLMToolCallContent(BaseModel):
method convert_parameters_to_dict (line 33) | def convert_parameters_to_dict(cls, v: Optional[Union[str, dict]]) -> ...
class LLMChatMessage (line 41) | class LLMChatMessage(BaseModel):
method check_content_type (line 49) | def check_content_type(self) -> Self:
FILE: kirara_ai/llm/format/request.py
class ResponseFormat (line 8) | class ResponseFormat(BaseModel):
class LLMChatRequest (line 11) | class LLMChatRequest(BaseModel):
FILE: kirara_ai/llm/format/rerank.py
class LLMReRankRequest (line 7) | class LLMReRankRequest(BaseModel):
method check (line 34) | def check(self) -> Self:
class ReRankerContent (line 39) | class ReRankerContent(BaseModel):
class LLMReRankResponse (line 51) | class LLMReRankResponse(BaseModel):
method sort_content (line 66) | def sort_content(self) -> Self:
FILE: kirara_ai/llm/format/response.py
class Message (line 9) | class Message(LLMChatMessage):
class Usage (line 13) | class Usage(BaseModel):
class LLMChatResponse (line 19) | class LLMChatResponse(BaseModel):
FILE: kirara_ai/llm/format/tool.py
class TextContent (line 6) | class TextContent(BaseModel):
class MediaContent (line 10) | class MediaContent(BaseModel):
class LLMToolResultContent (line 18) | class LLMToolResultContent(BaseModel):
class Function (line 32) | class Function(BaseModel):
method convert_arguments (line 41) | def convert_arguments(cls, v: Optional[Union[str, dict]]) -> Optional[...
class ToolCall (line 44) | class ToolCall(BaseModel):
class CallableWrapper (line 55) | class CallableWrapper(Generic[T]):
method __init__ (line 57) | def __init__(self, func: T):
method __call__ (line 60) | def __call__(self, *args, **kwargs) -> Coroutine[Any, Any, "LLMToolRes...
method __deepcopy__ (line 63) | def __deepcopy__(self, memo):
class ToolInputSchema (line 68) | class ToolInputSchema(BaseModel):
class Tool (line 83) | class Tool(BaseModel):
method serialize_invoke_func (line 106) | def serialize_invoke_func(self, invoke_func: CallableWrapper[ToolInvok...
FILE: kirara_ai/llm/llm_manager.py
class LLMManager (line 17) | class LLMManager:
method __init__ (line 30) | def __init__(
method load_config (line 46) | def load_config(self):
method load_backend (line 56) | def load_backend(self, backend_name: str):
method unload_backend (line 101) | async def unload_backend(self, backend_name: str):
method reload_backend (line 131) | async def reload_backend(self, backend_name: str):
method is_backend_available (line 139) | def is_backend_available(self, backend_name: str) -> bool:
method get (line 161) | def get(self, backend_name: str) -> Optional[LLMBackendAdapter]:
method get_llm (line 169) | def get_llm(self, model_id: str) -> Optional[LLMBackendAdapter]:
method get_supported_models (line 184) | def get_supported_models(self, model_type: ModelType, ability: ModelAb...
method get_llm_id_by_ability (line 198) | def get_llm_id_by_ability(self, ability: ModelAbility) -> Optional[str]:
method get_models_by_ability (line 208) | def get_models_by_ability(self, model_type: ModelType, ability: ModelA...
method get_models_by_type (line 220) | def get_models_by_type(self, model_type: ModelType) -> List[str]:
FILE: kirara_ai/llm/llm_registry.py
class LLMBackendRegistry (line 11) | class LLMBackendRegistry:
method __init__ (line 19) | def __init__(self):
method register (line 24) | def register(
method get (line 44) | def get(self, adapter_type: str) -> Optional[Type[LLMBackendAdapter]]:
method get_config_class (line 55) | def get_config_class(self, adapter_type: str) -> Optional[Type[BaseMod...
method get_adapter_types (line 66) | def get_adapter_types(self) -> list[str]:
method get_all_adapters (line 73) | def get_all_adapters(self) -> Dict[str, Type[LLMBackendAdapter]]:
FILE: kirara_ai/llm/model_types.py
class ModelType (line 5) | class ModelType(Enum):
method from_str (line 16) | def from_str(cls, value: str) -> "ModelType":
class ModelAbility (line 25) | class ModelAbility(Enum):
method is_capable (line 31) | def is_capable(self, ability: int) -> bool:
class LLMAbility (line 38) | class LLMAbility(ModelAbility):
method is_capable (line 60) | def is_capable(self, ability: int) -> bool:
class EmbeddingModelAbility (line 67) | class EmbeddingModelAbility(ModelAbility):
method is_capable (line 78) | def is_capable(self, ability: int) -> bool:
class ImageModelAbility (line 85) | class ImageModelAbility(ModelAbility):
method is_capable (line 96) | def is_capable(self, ability: int) -> bool:
class AudioModelAbility (line 102) | class AudioModelAbility(ModelAbility):
method is_capable (line 113) | def is_capable(self, ability: int) -> bool:
FILE: kirara_ai/logger.py
class LogBroadcaster (line 56) | class LogBroadcaster:
method __new__ (line 64) | def __new__(cls):
method __init__ (line 72) | def __init__(self):
method _setup_log_handler (line 78) | def _setup_log_handler(self):
method _broadcast_log (line 111) | def _broadcast_log(self, log_entry: Dict):
method subscribe (line 125) | def subscribe(self, callback: LogBroadcasterCallback) -> int:
method unsubscribe (line 136) | def unsubscribe(self, subscriber_id: int) -> bool:
method send_recent_logs (line 147) | def send_recent_logs(self, callback: LogBroadcasterCallback):
class WebSocketLogHandler (line 155) | class WebSocketLogHandler:
method add_websocket (line 162) | def add_websocket(cls, ws, loop: asyncio.AbstractEventLoop):
method remove_websocket (line 182) | def remove_websocket(cls, ws):
function init_log_broadcaster (line 193) | def init_log_broadcaster():
function get_logger (line 199) | def get_logger(tag: str):
class HypercornLoggerWrapper (line 208) | class HypercornLoggerWrapper:
method __init__ (line 209) | def __init__(self, logger):
method critical (line 212) | def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
method error (line 215) | def error(self, message: str, *args: Any, **kwargs: Any) -> None:
method warning (line 218) | def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
method info (line 221) | def info(self, message: str, *args: Any, **kwargs: Any) -> None:
method debug (line 226) | def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
method exception (line 229) | def exception(self, message: str, *args: Any, **kwargs: Any) -> None:
method log (line 232) | def log(self, level: int, message: str, *args: Any, **kwargs: Any) -> ...
function get_async_logger (line 236) | def get_async_logger(tag: str):
FILE: kirara_ai/mcp_module/manager.py
class ToolCacheEntry (line 19) | class ToolCacheEntry(NamedTuple):
class MCPServerManager (line 25) | class MCPServerManager:
method __init__ (line 28) | def __init__(self, container: DependencyContainer):
method load_servers (line 37) | def load_servers(self):
method load_server (line 46) | def load_server(self, server_config: MCPServerConfig) -> MCPServer:
method get_all_servers (line 53) | def get_all_servers(self) -> Dict[str, MCPServer]:
method get_server (line 57) | def get_server(self, server_id: str) -> Optional[MCPServer]:
method is_server_id_available (line 61) | def is_server_id_available(self, server_id: str) -> bool:
method get_statistics (line 75) | def get_statistics(self) -> Dict[str, int]:
method connect_all_servers (line 92) | def connect_all_servers(self, loop: asyncio.AbstractEventLoop):
method connect_server (line 108) | async def connect_server(self, server_id: str) -> bool:
method disconnect_all_servers (line 143) | def disconnect_all_servers(self, loop: asyncio.AbstractEventLoop):
method stop_server (line 157) | async def stop_server(self, server_id: str) -> bool:
method _update_tools_cache (line 189) | async def _update_tools_cache(self, server_id: str) -> bool:
method _remove_server_tools_from_cache (line 239) | def _remove_server_tools_from_cache(self, server_id: str):
method get_tools (line 255) | def get_tools(self) -> Dict[str, ToolCacheEntry]:
method get_tool_server (line 262) | def get_tool_server(self, tool_name: str) -> Optional[Tuple[MCPServer,...
method call_tool (line 282) | async def call_tool(self, tool_name: str, tool_args: dict) -> Optional...
method _update_prompts_cache (line 312) | async def _update_prompts_cache(self, server_id: str) -> bool:
method get_prompt_list (line 346) | async def get_prompt_list(self, server_id: str) -> Optional[list[types...
method get_prompt (line 362) | async def get_prompt(self, server_id: str, prompt_name: str, prompt_ar...
method _update_resources_cache (line 372) | async def _update_resources_cache(self, server_id: str) -> bool:
method get_resource_list (line 406) | async def get_resource_list(self, server_id: str) -> Optional[list[typ...
method get_resource (line 421) | async def get_resource(self, server_id: str, uri: str) -> Optional[typ...
method _handle_server_message (line 438) | async def _handle_server_message(self, server_id: str, message: Reques...
FILE: kirara_ai/mcp_module/models.py
class MCPConnectionState (line 4) | class MCPConnectionState(Enum):
FILE: kirara_ai/mcp_module/server.py
class MCPServer (line 19) | class MCPServer:
method __init__ (line 32) | def __init__(self, server_config: MCPServerConfig):
method connect (line 48) | async def connect(self):
method disconnect (line 88) | async def disconnect(self):
method _lifecycle_manager (line 123) | async def _lifecycle_manager(self):
method get_tools (line 183) | async def get_tools(self) -> types.ListToolsResult:
method call_tool (line 188) | async def call_tool(self, tool_name: str, tool_args: Optional[dict] = ...
method complete (line 202) | async def complete(self, prompt: str, tool_args: dict):
method get_prompt (line 218) | async def get_prompt(self, prompt_name: str, prompt_args: dict[str, st...
method list_prompts (line 232) | async def list_prompts(self) -> types.ListPromptsResult:
method list_resources (line 239) | async def list_resources(self) -> types.ListResourcesResult:
method list_resource_templates (line 244) | async def list_resource_templates(self) -> types.ListResourceTemplates...
method read_resource (line 249) | async def read_resource(self, uri: str) -> types.ReadResourceResult:
method subscribe_resource (line 262) | async def subscribe_resource(self, uri: str) -> types.EmptyResult:
method unsubscribe_resource (line 275) | async def unsubscribe_resource(self, uri: str) -> types.EmptyResult:
method message_handler_callback (line 288) | async def message_handler_callback(
method list_client_roots_callback (line 305) | async def list_client_roots_callback(self, ctx) -> types.ListRootsResu...
method send_ping (line 318) | async def send_ping(self) -> None:
method send_notification (line 322) | async def send_notification(self, notification: types.ClientNotificati...
method sampling_callback (line 334) | async def sampling_callback(self):
method logging_callback (line 338) | async def logging_callback(self):
FILE: kirara_ai/media/carrier/provider.py
class MediaReferenceProvider (line 6) | class MediaReferenceProvider(ABC, Generic[T]):
method get_reference_owner (line 10) | def get_reference_owner(self, reference_key: str) -> Optional[T]:
FILE: kirara_ai/media/carrier/registry.py
class MediaCarrierRegistry (line 8) | class MediaCarrierRegistry:
method __init__ (line 11) | def __init__(self, container: DependencyContainer):
method register (line 15) | def register(self, provider_name: str, provider_instance: MediaReferen...
method unregister (line 19) | def unregister(self, provider_name: str) -> None:
method get_provider (line 24) | def get_provider(self, provider_name: str) -> MediaReferenceProvider:
method get_all_providers (line 30) | def get_all_providers(self) -> Dict[str, MediaReferenceProvider]:
FILE: kirara_ai/media/carrier/service.py
class MediaCarrierService (line 10) | class MediaCarrierService:
method __init__ (line 13) | def __init__(self, container: DependencyContainer, media_manager: Medi...
method _build_reference_index (line 21) | def _build_reference_index(self) -> None:
method register_reference (line 33) | def register_reference(self, media_id: str, provider_name: str, refere...
method remove_reference (line 48) | def remove_reference(self, media_id: str, provider_name: str, referenc...
method get_reference_owner (line 64) | def get_reference_owner(self, reference_key: str) -> Optional[Any]:
method get_media_by_reference (line 76) | def get_media_by_reference(self, provider_name: str, reference_key: st...
method get_references_by_media (line 89) | def get_references_by_media(self, media_id: str) -> List[Tuple[str, st...
method cleanup_orphaned_references (line 104) | def cleanup_orphaned_references(self) -> int:
FILE: kirara_ai/media/manager.py
class MediaManager (line 25) | class MediaManager:
method __init__ (line 28) | def __init__(self, media_dir: str = "data/media"):
method _load_all_metadata (line 45) | def _load_all_metadata(self) -> None:
method _save_metadata (line 56) | def _save_metadata(self, metadata: MediaMetadata) -> None:
method _get_file_path (line 63) | def _get_file_path(self, media_id: str, format: str) -> Path:
method _create_task (line 67) | def _create_task(self, coro, name=None, loop=None):
method _save_file_async (line 76) | async def _save_file_async(self, data: bytes, target_path: Path):
method _download_file_async (line 81) | async def _download_file_async(self, url: str) -> bytes:
method _download_file_sync (line 95) | def _download_file_sync(self, url: str) -> bytes:
method register_media (line 109) | async def register_media(
method register_from_path (line 216) | async def register_from_path(
method register_from_url (line 238) | async def register_from_url(
method register_from_data (line 255) | async def register_from_data(
method add_reference (line 276) | def add_reference(self, media_id: str, reference_id: str) -> None:
method remove_reference (line 285) | def remove_reference(self, media_id: str, reference_id: str) -> None:
method delete_media (line 301) | def delete_media(self, media_id: str) -> None:
method update_metadata (line 324) | def update_metadata(
method add_tags (line 356) | def add_tags(self, media_id: str, tags: List[str]) -> None:
method remove_tags (line 368) | def remove_tags(self, media_id: str, tags: List[str]) -> None:
method get_metadata (line 380) | def get_metadata(self, media_id: str) -> Optional[MediaMetadata]:
method ensure_file_exists (line 384) | async def ensure_file_exists(self, media_id: str) -> Optional[Path]:
method get_file_path (line 466) | async def get_file_path(self, media_id: str) -> Optional[Path]:
method get_data (line 480) | async def get_data(self, media_id: str) -> Optional[bytes]:
method get_url (line 505) | async def get_url(self, media_id: str) -> Optional[str]:
method get_base64_url (line 524) | async def get_base64_url(self, media_id: str) -> Optional[str]:
method search_by_tags (line 539) | def search_by_tags(self, tags: List[str], match_all: bool = False) -> ...
method search_by_description (line 555) | def search_by_description(self, query: str) -> List[str]:
method search_by_source (line 565) | def search_by_source(self, source: str) -> List[str]:
method search_by_type (line 575) | def search_by_type(self, media_type: MediaType) -> List[str]:
method get_all_media_ids (line 585) | def get_all_media_ids(self) -> List[str]:
method cleanup_unreferenced (line 589) | def cleanup_unreferenced(self) -> int:
method create_media_message (line 598) | async def create_media_message(self, media_id: str) -> Optional["Media...
method get_media (line 616) | def get_media(self, media_id: str) -> Optional["Media"]:
method __new__ (line 623) | def __new__(cls, *args, **kwargs) -> "MediaManager":
method setup_cleanup_task (line 629) | def setup_cleanup_task(self, container: DependencyContainer):
FILE: kirara_ai/media/media_object.py
class Media (line 12) | class Media:
method __init__ (line 17) | def __init__(self, media_id: str, media_manager: MediaManager):
method media_type (line 31) | def media_type(self) -> MediaType:
method format (line 36) | def format(self) -> str:
method size (line 41) | def size(self) -> Optional[int]:
method description (line 46) | def description(self) -> Optional[str]:
method description (line 51) | def description(self, value: str) -> None:
method tags (line 56) | def tags(self) -> List[str]:
method mime_type (line 62) | def mime_type(self) -> str:
method add_tags (line 66) | def add_tags(self, tags: List[str]) -> None:
method remove_tags (line 70) | def remove_tags(self, tags: List[str]) -> None:
method add_reference (line 74) | def add_reference(self, reference_id: str) -> None:
method remove_reference (line 78) | def remove_reference(self, reference_id: str) -> None:
method get_file_path (line 82) | async def get_file_path(self) -> Path:
method get_data (line 88) | async def get_data(self) -> bytes:
method get_base64 (line 94) | async def get_base64(self) -> str:
method get_url (line 100) | async def get_url(self) -> str:
method get_base64_url (line 106) | async def get_base64_url(self) -> str:
method create_message (line 110) | async def create_message(self) -> "MediaMessage":
method __str__ (line 116) | def __str__(self) -> str:
method __repr__ (line 122) | def __repr__(self) -> str:
FILE: kirara_ai/media/metadata.py
class MediaMetadata (line 7) | class MediaMetadata:
method __init__ (line 10) | def __init__(
method to_dict (line 36) | def to_dict(self) -> Dict[str, Any]:
method mime_type (line 63) | def mime_type(self) -> str:
method from_dict (line 68) | def from_dict(cls, data: Dict[str, Any]) -> 'MediaMetadata':
FILE: kirara_ai/media/types/media_type.py
class MediaType (line 4) | class MediaType(Enum):
method from_mime (line 12) | def from_mime(cls, mime_type: str) -> 'MediaType':
FILE: kirara_ai/media/utils/mime.py
function detect_mime_type (line 16) | def detect_mime_type(data: Optional[bytes] = None, path: Optional[str] =...
FILE: kirara_ai/memory/composes/base.py
class MemoryComposer (line 15) | class MemoryComposer(ABC):
method compose (line 21) | def compose(
class MemoryDecomposer (line 27) | class MemoryDecomposer(ABC):
method decompose (line 33) | def decompose(self, entries: List[MemoryEntry]) -> List[ComposableMess...
method empty_message (line 37) | def empty_message(self) -> ComposableMessageType:
FILE: kirara_ai/memory/composes/builtin_composes.py
class DefaultMemoryComposer (line 16) | class DefaultMemoryComposer(MemoryComposer):
method __init__ (line 17) | def __init__(self):
method compose (line 20) | def compose(
class DefaultMemoryDecomposer (line 59) | class DefaultMemoryDecomposer(MemoryDecomposer):
method __init__ (line 60) | def __init__(self):
method decompose (line 63) | def decompose(self, entries: List[MemoryEntry]) -> List[ComposableMess...
class MultiElementDecomposer (line 77) | class MultiElementDecomposer(MemoryDecomposer):
method __init__ (line 80) | def __init__(self):
method decompose (line 83) | def decompose(self, entries: List[MemoryEntry]) -> List[Union[IMMessag...
FILE: kirara_ai/memory/composes/composer_strategy.py
function drop_think_part (line 14) | def drop_think_part(text: str) -> str:
class MessageProcessor (line 20) | class MessageProcessor(ABC):
method __init__ (line 23) | def __init__(self, container: DependencyContainer):
method process (line 27) | def process(self, message: Any, context: Dict) -> str:
class TextMessageProcessor (line 31) | class TextMessageProcessor(MessageProcessor):
method process (line 34) | def process(self, message: TextMessage, context: Dict) -> str:
class MediaMessageProcessor (line 38) | class MediaMessageProcessor(MessageProcessor):
method process (line 41) | def process(self, message: MediaMessage, context: Dict) -> str:
class LLMChatTextContentProcessor (line 50) | class LLMChatTextContentProcessor(MessageProcessor):
method process (line 53) | def process(self, content: LLMChatTextContent, context: Dict) -> str:
class LLMChatImageContentProcessor (line 57) | class LLMChatImageContentProcessor(MessageProcessor):
method process (line 60) | def process(self, content: LLMChatImageContent, context: Dict) -> str:
class LLMToolCallContentProcessor (line 72) | class LLMToolCallContentProcessor(MessageProcessor):
method process (line 75) | def process(self, content: LLMToolCallContent, context: Dict) -> str:
class LLMToolResultContentProcessor (line 86) | class LLMToolResultContentProcessor(MessageProcessor):
method process (line 89) | def process(self, content: LLMToolResultContent, context: Dict) -> str:
class IMMessageProcessor (line 122) | class IMMessageProcessor(MessageProcessor):
method __init__ (line 125) | def __init__(self, container: DependencyContainer):
method process (line 132) | def process(self, message: IMMessage, context: Dict) -> str:
class LLMChatMessageProcessor (line 146) | class LLMChatMessageProcessor(MessageProcessor):
method __init__ (line 149) | def __init__(self, container: DependencyContainer):
method process (line 158) | def process(self, message: LLMChatMessage, context: Dict) -> str:
class ProcessorFactory (line 179) | class ProcessorFactory:
method __init__ (line 182) | def __init__(self, container: DependencyContainer):
method get_processor (line 189) | def get_processor(self, message_type: Type) -> Optional[MessageProcess...
FILE: kirara_ai/memory/composes/decomposer_strategy.py
class ContentInfo (line 15) | class ContentInfo(NamedTuple):
class ContentParseStrategy (line 24) | class ContentParseStrategy(Protocol):
method extract_content (line 27) | def extract_content(self, content: str, entry: MemoryEntry) -> List[Co...
method to_llm_content (line 31) | def to_llm_content(self, info: ContentInfo) -> LLMChatContentPartType:
method to_text (line 35) | def to_text(self, info: ContentInfo) -> str:
class TextContentStrategy (line 40) | class TextContentStrategy:
method extract_content (line 43) | def extract_content(self, content: str, entry: MemoryEntry) -> List[Co...
method to_llm_content (line 83) | def to_llm_content(self, info: ContentInfo) -> LLMChatContentPartType:
method to_text (line 86) | def to_text(self, info: ContentInfo) -> str:
class MediaContentStrategy (line 90) | class MediaContentStrategy:
method __init__ (line 93) | def __init__(self):
method extract_content (line 96) | def extract_content(self, content: str, entry: MemoryEntry) -> List[Co...
method to_llm_content (line 115) | def to_llm_content(self, info: ContentInfo) -> LLMChatContentPartType:
method to_text (line 118) | def to_text(self, info: ContentInfo) -> str:
class ToolCallContentStrategy (line 122) | class ToolCallContentStrategy:
method extract_content (line 125) | def extract_content(self, content: str, entry: MemoryEntry) -> List[Co...
method to_llm_content (line 151) | def to_llm_content(self, info: ContentInfo) -> LLMChatContentPartType:
method to_text (line 154) | def to_text(self, info: ContentInfo) -> str:
class ToolResultContentStrategy (line 158) | class ToolResultContentStrategy:
method extract_content (line 161) | def extract_content(self, content: str, entry: MemoryEntry) -> List[Co...
method to_llm_content (line 187) | def to_llm_content(self, info: ContentInfo) -> LLMChatContentPartType:
method to_text (line 190) | def to_text(self, info: ContentInfo) -> str:
class ContentParser (line 194) | class ContentParser:
method __init__ (line 197) | def __init__(self):
method parse_content (line 205) | def parse_content(self, content: str, entry: MemoryEntry) -> List[Cont...
method to_llm_message (line 216) | def to_llm_message(self, content_infos: List[ContentInfo], role: RoleT...
method to_text (line 263) | def to_text(self, content_infos: List[ContentInfo]) -> str:
class DefaultDecomposerStrategy (line 273) | class DefaultDecomposerStrategy:
method __init__ (line 276) | def __init__(self):
method decompose (line 279) | def decompose(self, entries: List[MemoryEntry], context: Dict[str, Any...
method _get_time_str (line 321) | def _get_time_str(self, time_diff: timedelta) -> str:
class MultiElementDecomposerStrategy (line 333) | class MultiElementDecomposerStrategy:
method __init__ (line 336) | def __init__(self):
method decompose (line 339) | def decompose(self, entries: List[MemoryEntry], context: Dict[str, Any...
method _process_entry (line 353) | def _process_entry(self, entry: MemoryEntry) -> List[LLMChatMessage]:
method _merge_adjacent_messages (line 389) | def _merge_adjacent_messages(self, messages: List[LLMChatMessage]) -> ...
FILE: kirara_ai/memory/composes/xml_helper.py
class XMLHelper (line 5) | class XMLHelper:
method escape_xml_attr (line 9) | def escape_xml_attr(text: str) -> str:
method unescape_xml_attr (line 16) | def unescape_xml_attr(text: str) -> str:
method create_xml_tag (line 21) | def create_xml_tag(tag_name: str, attributes: Dict[str, Optional[str]]...
method parse_xml_tag (line 30) | def parse_xml_tag(content: str, tag_name: str) -> List[Tuple[Dict[str,...
method get_attr (line 46) | def get_attr(attrs: Dict[str, Optional[str]], key: str, default: Optio...
FILE: kirara_ai/memory/entry.py
class MemoryEntry (line 9) | class MemoryEntry:
FILE: kirara_ai/memory/memory_manager.py
class MemoryManager (line 19) | class MemoryManager(MediaReferenceProvider[List[MemoryEntry]]):
method __init__ (line 22) | def __init__(
method _init_persistence (line 49) | def _init_persistence(self):
method register_scope (line 64) | def register_scope(self, name: str, scope_class: Type[MemoryScope]):
method register_composer (line 68) | def register_composer(self, name: str, composer_class: Type[MemoryComp...
method register_decomposer (line 72) | def register_decomposer(self, name: str, decomposer_class: Type[Memory...
method store (line 76) | def store(self, scope: MemoryScope, entry: MemoryEntry, extra_identifi...
method query (line 100) | def query(self, scope: MemoryScope, sender: ChatSender, extra_identifi...
method shutdown (line 121) | def shutdown(self):
method clear_memory (line 130) | def clear_memory(self, scope: MemoryScope, sender: ChatSender) -> None:
method get_reference_owner (line 148) | def get_reference_owner(self, reference_key: str) -> Optional[List[Mem...
method _register_media_reference (line 154) | def _register_media_reference(self, entry: MemoryEntry, reference_key:...
method _remove_media_references (line 160) | def _remove_media_references(self, removed_entries: List[MemoryEntry],...
FILE: kirara_ai/memory/persistences/base.py
class MemoryPersistence (line 10) | class MemoryPersistence(ABC):
method save (line 14) | def save(self, scope_key: str, entries: List[MemoryEntry]) -> None:
method load (line 18) | def load(self, scope_key: str) -> List[MemoryEntry]:
method flush (line 22) | def flush(self) -> None:
class AsyncMemoryPersistence (line 26) | class AsyncMemoryPersistence:
method __init__ (line 29) | def __init__(self, persistence: MemoryPersistence):
method _worker (line 36) | def _worker(self):
method load (line 49) | def load(self, scope_key: str) -> List[MemoryEntry]:
method save (line 52) | def save(self, scope_key: str, entries: List[MemoryEntry]):
method stop (line 55) | def stop(self):
FILE: kirara_ai/memory/persistences/codecs.py
class MemoryJSONEncoder (line 9) | class MemoryJSONEncoder(json.JSONEncoder):
method default (line 10) | def default(self, obj):
function memory_json_decoder (line 40) | def memory_json_decoder(obj):
FILE: kirara_ai/memory/persistences/file_persistence.py
class FileMemoryPersistence (line 12) | class FileMemoryPersistence(MemoryPersistence):
method __init__ (line 15) | def __init__(self, data_dir: str):
method _get_file_path (line 22) | def _get_file_path(self, scope_key: str) -> str:
method save (line 26) | def save(self, scope_key: str, entries: List[MemoryEntry]) -> None:
method load (line 50) | def load(self, scope_key: str) -> List[MemoryEntry]:
method flush (line 74) | def flush(self) -> None:
FILE: kirara_ai/memory/persistences/redis_persistence.py
class RedisMemoryPersistence (line 11) | class RedisMemoryPersistence(MemoryPersistence):
method __init__ (line 14) | def __init__(
method save (line 28) | def save(self, scope_key: str, entries: List[MemoryEntry]) -> None:
method load (line 46) | def load(self, scope_key: str) -> List[MemoryEntry]:
method flush (line 68) | def flush(self) -> None:
FILE: kirara_ai/memory/registry.py
class Registry (line 9) | class Registry:
method __init__ (line 15) | def __init__(self, container: DependencyContainer):
method register (line 19) | def register(self, name: str, cls: Type) -> None:
method unregister (line 23) | def unregister(self, name: str) -> None:
class ScopeRegistry (line 29) | class ScopeRegistry(Registry):
method get_scope (line 32) | def get_scope(self, name: str) -> MemoryScope:
class ComposerRegistry (line 39) | class ComposerRegistry(Registry):
method get_composer (line 42) | def get_composer(self, name: str) -> MemoryComposer:
class DecomposerRegistry (line 49) | class DecomposerRegistry(Registry):
method get_decomposer (line 52) | def get_decomposer(self, name: str) -> MemoryDecomposer:
FILE: kirara_ai/memory/scopes/base.py
class MemoryScope (line 6) | class MemoryScope(ABC):
method get_scope_key (line 10) | def get_scope_key(self, sender: ChatSender) -> str:
method is_in_scope (line 14) | def is_in_scope(self, target_sender: ChatSender, query_sender: ChatSen...
FILE: kirara_ai/memory/scopes/builtin_scopes.py
class MemberScope (line 7) | class MemberScope(MemoryScope):
method get_scope_key (line 10) | def get_scope_key(self, sender: ChatSender) -> str:
method is_in_scope (line 16) | def is_in_scope(self, target_sender: ChatSender, query_sender: ChatSen...
class GroupScope (line 29) | class GroupScope(MemoryScope):
method get_scope_key (line 32) | def get_scope_key(self, sender: ChatSender) -> str:
method is_in_scope (line 38) | def is_in_scope(self, target_sender: ChatSender, query_sender: ChatSen...
class GlobalScope (line 48) | class GlobalScope(MemoryScope):
method get_scope_key (line 51) | def get_scope_key(self, sender: ChatSender) -> str:
method is_in_scope (line 54) | def is_in_scope(self, target_sender: ChatSender, query_sender: ChatSen...
FILE: kirara_ai/plugin_manager/models.py
class PluginInfo (line 6) | class PluginInfo(BaseModel):
FILE: kirara_ai/plugin_manager/plugin.py
class Plugin (line 10) | class Plugin(ABC):
method on_load (line 35) | def on_load(self):
method on_start (line 39) | def on_start(self):
method on_stop (line 43) | def on_stop(self):
FILE: kirara_ai/plugin_manager/plugin_event_bus.py
class PluginEventBus (line 6) | class PluginEventBus:
method __init__ (line 7) | def __init__(self, event_bus: EventBus):
method register (line 11) | def register(self, event_type: Type, listener: Callable):
method unregister (line 15) | def unregister(self, event_type: Type, listener: Callable):
method post (line 18) | def post(self, event):
method unregister_all (line 21) | def unregister_all(self):
FILE: kirara_ai/plugin_manager/plugin_loader.py
class PluginLoader (line 18) | class PluginLoader:
method __init__ (line 19) | def __init__(self, container: DependencyContainer, plugin_dir: str):
method register_plugin (line 30) | def register_plugin(self, plugin_class: Type[Plugin], plugin_name: Opt...
method discover_internal_plugins (line 51) | def discover_internal_plugins(self, plugin_dir=None):
method load_plugin (line 71) | def load_plugin(self, plugin_name: str):
method _load_internal_plugin (line 82) | def _load_internal_plugin(self, plugin_name: str):
method _load_external_plugin (line 110) | def _load_external_plugin(self, plugin_name: str):
method instantiate_plugin (line 151) | def instantiate_plugin(self, plugin_class):
method load_plugins (line 159) | def load_plugins(self):
method start_plugins (line 172) | def start_plugins(self):
method stop_plugins (line 186) | def stop_plugins(self):
method get_plugin_info (line 201) | def get_plugin_info(self, plugin_name: str) -> Optional[PluginInfo]:
method get_all_plugin_infos (line 205) | def get_all_plugin_infos(self) -> List[PluginInfo]:
method install_plugin (line 209) | async def install_plugin(
method uninstall_plugin (line 263) | async def uninstall_plugin(self, plugin_name: str) -> bool:
method enable_plugin (line 322) | async def enable_plugin(self, plugin_name: str) -> bool:
method disable_plugin (line 356) | async def disable_plugin(self, plugin_name: str) -> bool:
method update_plugin (line 391) | async def update_plugin(self, plugin_name: str, new_package_name: Opti...
method discover_external_plugins (line 465) | def discover_external_plugins(self):
FILE: kirara_ai/plugin_manager/utils.py
function get_package_metadata (line 5) | def get_package_metadata(package_name: str) -> Optional[Dict[str, Any]]:
FILE: kirara_ai/plugins/bundled_frpc/__init__.py
class FrpcPlugin (line 14) | class FrpcPlugin(Plugin):
method __init__ (line 23) | def __init__(self):
method on_load (line 26) | def on_load(self):
method on_start (line 35) | def on_start(self):
method on_stop (line 46) | def on_stop(self):
FILE: kirara_ai/plugins/bundled_frpc/frpc_manager.py
class FrpcManager (line 24) | class FrpcManager:
method __init__ (line 27) | def __init__(self, global_config: GlobalConfig):
method download_frpc (line 53) | async def download_frpc(self, progress_callback: Optional[Callable[[fl...
method _get_frpc_version (line 180) | def _get_frpc_version(self):
method _generate_config (line 198) | def _generate_config(self, web_port: int) -> bool:
method start_frpc (line 249) | def start_frpc(self, web_port: int) -> bool:
method stop_frpc (line 310) | def stop_frpc(self) -> bool:
method _calculate_remote_url (line 336) | def _calculate_remote_url(self):
method get_status (line 345) | def get_status(self) -> Tuple[bool, str, str, str, float]:
method is_installed (line 371) | def is_installed(self) -> bool:
FILE: kirara_ai/plugins/bundled_frpc/models.py
class FrpcStatus (line 8) | class FrpcStatus(BaseModel):
class FrpcConfigUpdate (line 19) | class FrpcConfigUpdate(BaseModel):
class FrpcDownloadProgress (line 28) | class FrpcDownloadProgress(BaseModel):
FILE: kirara_ai/plugins/bundled_frpc/routes.py
function get_status (line 17) | async def get_status():
function update_config (line 38) | async def update_config():
function start_frpc (line 88) | async def start_frpc():
function stop_frpc (line 119) | async def stop_frpc():
function download_frpc (line 149) | async def download_frpc():
FILE: kirara_ai/plugins/im_http_legacy_adapter/__init__.py
class HttpLegacyAdapterPlugin (line 12) | class HttpLegacyAdapterPlugin(Plugin):
method __init__ (line 16) | def __init__(self):
method on_load (line 19) | def on_load(self):
method on_start (line 33) | def on_start(self):
method on_stop (line 36) | def on_stop(self):
FILE: kirara_ai/plugins/im_http_legacy_adapter/adapter.py
class HttpLegacyConfig (line 23) | class HttpLegacyConfig(BaseModel):
class ResponseResult (line 35) | class ResponseResult:
method __init__ (line 36) | def __init__(self, message=None, voice=None, image=None, result_status...
method to_dict (line 52) | def to_dict(self):
method pop_all (line 60) | def pop_all(self):
class MessageHandler (line 66) | class MessageHandler(Protocol):
method __call__ (line 67) | async def __call__(self, message: IMMessage) -> None: ...
class V2Request (line 70) | class V2Request:
method __init__ (line 71) | def __init__(self, session_id: str, username: str, message: str, reque...
class HttpLegacyAdapter (line 81) | class HttpLegacyAdapter(IMAdapter):
method __init__ (line 87) | def __init__(self, config: HttpLegacyConfig):
method convert_to_message (line 93) | async def convert_to_message(self, raw_message: Any) -> IMMessage:
method handle_message_elements (line 119) | async def handle_message_elements(self, result: ResponseResult, messag...
method verify_api_key (line 128) | def verify_api_key(self, request: Request) -> bool:
method create_auth_error_response (line 140) | def create_auth_error_response(self):
method setup_routes (line 148) | def setup_routes(self, target_app=None):
method send_message (line 228) | async def send_message(self, message: IMMessage, recipient: ChatSender):
method is_standalone (line 233) | def is_standalone(self):
method _start_standalone_server (line 236) | async def _start_standalone_server(self):
method start (line 255) | async def start(self):
method cleanup_expired_requests (line 277) | async def cleanup_expired_requests(self):
method stop (line 290) | async def stop(self):
FILE: kirara_ai/plugins/im_http_legacy_adapter/tests/api_test.py
class FakeWorkflowDispatcher (line 24) | class FakeWorkflowDispatcher(WorkflowDispatcher):
method dispatch (line 25) | async def dispatch(self, source: IMAdapter, message: IMMessage):
function config (line 30) | def config():
function adapter (line 35) | def adapter(config):
function test_chat_endpoint (line 49) | async def test_chat_endpoint(adapter):
function test_response_result (line 73) | async def test_response_result():
function test_adapter_lifecycle (line 94) | async def test_adapter_lifecycle(adapter):
FILE: kirara_ai/plugins/im_qqbot_adapter/__init__.py
class QQBotAdapterPlugin (line 13) | class QQBotAdapterPlugin(Plugin):
method __init__ (line 16) | def __init__(self):
method on_load (line 19) | def on_load(self):
method on_start (line 33) | def on_start(self):
method on_stop (line 36) | def on_stop(self):
FILE: kirara_ai/plugins/im_qqbot_adapter/adapter.py
function make_webhook_url (line 27) | def make_webhook_url():
function auto_generate_webhook_url (line 31) | def auto_generate_webhook_url(s: dict):
class QQBotConfig (line 37) | class QQBotConfig(BaseModel):
function patched_post_file (line 55) | async def patched_post_file(
class QQBotAdapter (line 80) | class QQBotAdapter(botpy.WebHookClient, IMAdapter, BotProfileAdapter):
method __init__ (line 89) | def __init__(self, config: QQBotConfig):
method convert_to_message (line 102) | async def convert_to_message(self, raw_message: ymbotpy.message.BaseMe...
method send_message (line 141) | async def send_message(self, message: IMMessage, recipient: ChatSender):
method on_c2c_message_create (line 236) | async def on_c2c_message_create(self, message: ymbotpy.message.C2CMess...
method on_group_at_message_create (line 245) | async def on_group_at_message_create(self, message: ymbotpy.message.Gr...
method get_bot_profile (line 257) | async def get_bot_profile(self) -> Optional[UserProfile]:
method start (line 271) | async def start(self):
method stop (line 293) | async def stop(self):
FILE: kirara_ai/plugins/im_telegram_adapter/__init__.py
class TelegramAdapterPlugin (line 13) | class TelegramAdapterPlugin(Plugin):
method __init__ (line 16) | def __init__(self):
method on_load (line 19) | def on_load(self):
method on_start (line 34) | def on_start(self):
method on_stop (line 37) | def on_stop(self):
FILE: kirara_ai/plugins/im_telegram_adapter/adapter.py
function get_display_name (line 21) | def get_display_name(user: User | ChatFullInfo):
class TelegramConfig (line 30) | class TelegramConfig(BaseModel):
method __repr__ (line 38) | def __repr__(self):
class TelegramAdapter (line 42) | class TelegramAdapter(IMAdapter, UserProfileAdapter, EditStateAdapter, B...
method __init__ (line 48) | def __init__(self, config: TelegramConfig):
method command_start (line 63) | async def command_start(self, update: Update, context: ContextTypes.DE...
method handle_message (line 68) | async def handle_message(self, update: Update, context: ContextTypes.D...
method convert_to_message (line 81) | async def convert_to_message(self, raw_message: Update) -> IMMessage:
method send_message (line 185) | async def send_message(self, message: IMMessage, recipient: ChatSender):
method start (line 236) | async def start(self):
method stop (line 246) | async def stop(self):
method set_chat_editing_state (line 258) | async def set_chat_editing_state(
method _cached_get_chat (line 292) | async def _cached_get_chat(self, user_id):
method query_user_profile (line 300) | async def query_user_profile(self, chat_sender: ChatSender) -> UserPro...
method get_bot_profile (line 330) | async def get_bot_profile(self) -> Optional[UserProfile]:
FILE: kirara_ai/plugins/im_wecom_adapter/__init__.py
class WecomAdapterPlugin (line 13) | class WecomAdapterPlugin(Plugin):
method __init__ (line 16) | def __init__(self):
method on_load (line 19) | def on_load(self):
method on_start (line 33) | def on_start(self):
method on_stop (line 36) | def on_stop(self):
FILE: kirara_ai/plugins/im_wecom_adapter/adapter.py
function make_webhook_url (line 31) | def make_webhook_url():
function auto_generate_webhook_url (line 35) | def auto_generate_webhook_url(s: dict):
class WecomConfig (line 41) | class WecomConfig(BaseModel):
method __init__ (line 67) | def __init__(self, **kwargs: Any):
class WeComUtils (line 74) | class WeComUtils:
method __init__ (line 77) | def __init__(self, client: BaseWeChatClient):
method access_token (line 82) | def access_token(self) -> Optional[str]:
method download_and_save_media (line 85) | async def download_and_save_media(self, media_id: str, file_name: str)...
method download_media (line 99) | async def download_media(self, media_id: str) -> Optional[bytes]:
class WecomAdapter (line 114) | class WecomAdapter(IMAdapter):
method __init__ (line 120) | def __init__(self, config: WecomConfig):
method setup_wechat_api (line 144) | def setup_wechat_api(self):
method setup_routes (line 156) | def setup_routes(self):
method convert_to_message (line 239) | async def convert_to_message(self, raw_message: Any, media_path: Optio...
method _send_text (line 274) | async def _send_text(self, user_id: str, text: str):
method _send_media (line 283) | async def _send_media(self, user_id: str, media_data: str, media_type:...
method send_message (line 293) | async def send_message(self, message: IMMessage, recipient: ChatSender):
method _start_standalone_server (line 323) | async def _start_standalone_server(self):
method _stop_standalone_server (line 339) | async def _stop_standalone_server(self):
method start (line 350) | async def start(self):
method stop (line 359) | async def stop(self):
FILE: kirara_ai/plugins/im_wecom_adapter/delegates.py
class WechatApiDelegate (line 12) | class WechatApiDelegate(ABC):
method setup_api (line 16) | def setup_api(self, config: "WecomConfig"):
method check_signature (line 20) | def check_signature(self, signature: str, timestamp: str, nonce: str, ...
method decrypt_message (line 24) | def decrypt_message(self, message: bytes, signature: str, timestamp: s...
method parse_message (line 28) | def parse_message(self, message: str) -> BaseMessage:
method send_text (line 32) | async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
method send_media (line 36) | async def send_media(self, app_id: str, user_id: str, media_type: str,...
class CorpWechatApiDelegate (line 40) | class CorpWechatApiDelegate(WechatApiDelegate):
method setup_api (line 43) | def setup_api(self, config: "WecomConfig"):
method check_signature (line 56) | def check_signature(self, signature: str, timestamp: str, nonce: str, ...
method decrypt_message (line 60) | def decrypt_message(self, message: bytes, signature: str, timestamp: s...
method parse_message (line 64) | def parse_message(self, message: str) -> BaseMessage:
method send_text (line 68) | async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
method send_media (line 72) | async def send_media(self, app_id: str, user_id: str, media_type: str,...
class PublicWechatApiDelegate (line 79) | class PublicWechatApiDelegate(WechatApiDelegate):
method setup_api (line 82) | def setup_api(self, config: "WecomConfig"):
method check_signature (line 95) | def check_signature(self, signature: str, timestamp: str, nonce: str, ...
method decrypt_message (line 101) | def decrypt_message(self, message: bytes, signature: str, timestamp: s...
method parse_message (line 105) | def parse_message(self, message: str) -> BaseMessage:
method send_text (line 109) | async def send_text(self, app_id: str, user_id: str, text: str) -> Any:
method send_media (line 114) | async def send_media(self, app_id: str, user_id: str, media_type: str,...
FILE: kirara_ai/plugins/llm_preset_adapters/__init__.py
class LLMPresetAdaptersPlugin (line 22) | class LLMPresetAdaptersPlugin(Plugin):
method __init__ (line 23) | def __init__(self):
method on_load (line 26) | def on_load(self):
method on_start (line 68) | def on_start(self):
method on_stop (line 71) | def on_stop(self):
FILE: kirara_ai/plugins/llm_preset_adapters/alibabacloud_adapter.py
class AlibabaCloudConfig (line 7) | class AlibabaCloudConfig(OpenAIConfig):
class AlibabaCloudAdapter (line 11) | class AlibabaCloudAdapter(OpenAIAdapter):
method __init__ (line 12) | def __init__(self, config: AlibabaCloudConfig):
method auto_detect_models (line 15) | async def auto_detect_models(self) -> list[ModelConfig]:
FILE: kirara_ai/plugins/llm_preset_adapters/claude_adapter.py
class ClaudeConfig (line 22) | class ClaudeConfig(BaseModel):
function convert_llm_chat_message_to_claude_message (line 28) | async def convert_llm_chat_message_to_claude_message(messages: list[LLMC...
function convert_tools_to_claude_format (line 50) | def convert_tools_to_claude_format(tools: list[Tool]) -> list[dict]:
function resolve_tool_result (line 54) | async def resolve_tool_result(element: LLMToolResultContent, media_manag...
class ClaudeAdapter (line 72) | class ClaudeAdapter(LLMBackendAdapter, AutoDetectModelsProtocol, LLMChat...
method __init__ (line 76) | def __init__(self, config: ClaudeConfig):
method chat (line 81) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
method auto_detect_models (line 156) | async def auto_detect_models(self) -> list[str]:
FILE: kirara_ai/plugins/llm_preset_adapters/deepseek_adapter.py
class DeepSeekConfig (line 4) | class DeepSeekConfig(OpenAIConfig):
class DeepSeekAdapter (line 8) | class DeepSeekAdapter(OpenAIAdapterChatBase):
method __init__ (line 9) | def __init__(self, config: DeepSeekConfig):
FILE: kirara_ai/plugins/llm_preset_adapters/gemini_adapter.py
class GeminiConfig (line 49) | class GeminiConfig(BaseModel):
function convert_non_tool_message (line 55) | async def convert_non_tool_message(msg: LLMChatMessage, media_manager: M...
function convert_llm_chat_message_to_gemini_message (line 83) | async def convert_llm_chat_message_to_gemini_message(msg: LLMChatMessage...
function convert_all_messages_to_gemini_format (line 92) | async def convert_all_messages_to_gemini_format(messages: List[LLMChatMe...
function convert_tools_to_gemini_format (line 96) | def convert_tools_to_gemini_format(tools: list[Tool]) -> list[dict[Liter...
function resolve_tool_results (line 147) | def resolve_tool_results(element: LLMToolResultContent) -> dict:
class GeminiAdapter (line 163) | class GeminiAdapter(LLMBackendAdapter, AutoDetectModelsProtocol, LLMChat...
method __init__ (line 167) | def __init__(self, config: GeminiConfig):
method chat (line 172) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
method embed (line 254) | def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse:
method auto_detect_models (line 293) | async def auto_detect_models(self) -> list[ModelConfig]:
method _post_with_retry (line 309) | def _post_with_retry(self, url: str, json: dict, headers: dict, retry_...
FILE: kirara_ai/plugins/llm_preset_adapters/minimax_adapter.py
class MinimaxConfig (line 4) | class MinimaxConfig(OpenAIConfig):
class MinimaxAdapter (line 7) | class MinimaxAdapter(OpenAIAdapterChatBase):
method __init__ (line 8) | def __init__(self, config: MinimaxConfig):
FILE: kirara_ai/plugins/llm_preset_adapters/mistral_adapter.py
class MistralConfig (line 8) | class MistralConfig(OpenAIConfig):
class MistralAdapter (line 12) | class MistralAdapter(OpenAIAdapter, AutoDetectModelsProtocol):
method __init__ (line 13) | def __init__(self, config: MistralConfig):
method auto_detect_models (line 16) | async def auto_detect_models(self) -> list[str]:
FILE: kirara_ai/plugins/llm_preset_adapters/moonshot_adapter.py
class MoonshotConfig (line 6) | class MoonshotConfig(OpenAIConfig):
class MoonshotAdapter (line 10) | class MoonshotAdapter(OpenAIAdapterChatBase):
method __init__ (line 11) | def __init__(self, config: MoonshotConfig):
FILE: kirara_ai/plugins/llm_preset_adapters/ollama_adapter.py
class OllamaConfig (line 25) | class OllamaConfig(BaseModel):
function resolve_media_ids (line 30) | async def resolve_media_ids(media_ids: list[str], media_manager: MediaMa...
function convert_llm_response (line 39) | def convert_llm_response(response_data: dict[str, dict[str, Any]]) -> li...
function convert_non_tool_message (line 51) | def convert_non_tool_message(msg: LLMChatMessage, media_manager: MediaMa...
function convert_tool_result_message (line 80) | def convert_tool_result_message(msg: LLMChatMessage, media_manager: Medi...
function convert_tools_to_ollama_format (line 99) | def convert_tools_to_ollama_format(tools: list[Tool]) -> list[dict]:
class OllamaAdapter (line 103) | class OllamaAdapter(LLMBackendAdapter, AutoDetectModelsProtocol, LLMChat...
method __init__ (line 104) | def __init__(self, config: OllamaConfig):
method chat (line 109) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
method embed (line 171) | def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse:
method auto_detect_models (line 199) | async def auto_detect_models(self) -> list[ModelConfig]:
FILE: kirara_ai/plugins/llm_preset_adapters/openai_adapter.py
function convert_parts_factory (line 25) | async def convert_parts_factory(messages: LLMChatMessage, media_manager:...
function convert_llm_chat_message_to_openai_message (line 84) | async def convert_llm_chat_message_to_openai_message(messages: list[LLMC...
function convert_tools_to_openai_format (line 90) | def convert_tools_to_openai_format(tools: list[Tool]) -> list[dict]:
class OpenAIConfig (line 101) | class OpenAIConfig(BaseModel):
class OpenAIAdapterChatBase (line 107) | class OpenAIAdapterChatBase(LLMBackendAdapter, AutoDetectModelsProtocol,...
method __init__ (line 110) | def __init__(self, config: OpenAIConfig):
method chat (line 113) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
method get_models (line 184) | async def get_models(self) -> list[str]:
method auto_detect_models (line 195) | async def auto_detect_models(self) -> list[ModelConfig]:
class EmbeddingData (line 205) | class EmbeddingData(TypedDict):
class EmbeddingResponse (line 210) | class EmbeddingResponse(TypedDict):
class OpenAIAdapter (line 217) | class OpenAIAdapter(OpenAIAdapterChatBase, LLMEmbeddingProtocol):
method embed (line 218) | def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse:
FILE: kirara_ai/plugins/llm_preset_adapters/openrouter_adapter.py
class OpenRouterConfig (line 9) | class OpenRouterConfig(OpenAIConfig):
class OpenRouterAdapter (line 12) | class OpenRouterAdapter(OpenAIAdapter):
method __init__ (line 13) | def __init__(self, config: OpenRouterConfig):
method auto_detect_models (line 16) | async def auto_detect_models(self) -> list[ModelConfig]:
FILE: kirara_ai/plugins/llm_preset_adapters/siliconflow_adapter.py
class SiliconFlowConfig (line 6) | class SiliconFlowConfig(OpenAIConfig):
class SiliconFlowAdapter (line 10) | class SiliconFlowAdapter(OpenAIAdapter):
method __init__ (line 11) | def __init__(self, config: SiliconFlowConfig):
method auto_detect_models (line 14) | async def auto_detect_models(self) -> list[str]:
FILE: kirara_ai/plugins/llm_preset_adapters/tencentcloud_adapter.py
class TencentCloudConfig (line 5) | class TencentCloudConfig(OpenAIConfig):
class TencentCloudAdapter (line 9) | class TencentCloudAdapter(OpenAIAdapter):
method __init__ (line 10) | def __init__(self, config: TencentCloudConfig):
FILE: kirara_ai/plugins/llm_preset_adapters/tests/test_utils.py
function test_guess_openai_model (line 267) | def test_guess_openai_model():
FILE: kirara_ai/plugins/llm_preset_adapters/utils.py
function generate_tool_call_id (line 9) | def generate_tool_call_id(name: str) -> str:
function pick_tool_calls (line 12) | def pick_tool_calls(calls: list[LLMChatContentPartType]) -> Optional[lis...
function guess_openai_model (line 24) | def guess_openai_model(model_id: str) -> Tuple[ModelType, int] | None:
function guess_qwen_model (line 126) | def guess_qwen_model(model_id: str) -> Tuple[ModelType, int] | None:
FILE: kirara_ai/plugins/llm_preset_adapters/volcengine_adapter.py
class VolcengineConfig (line 15) | class VolcengineConfig(OpenAIConfig):
function generate_volcengine_signature (line 20) | def generate_volcengine_signature(access_key_id, access_key_secret, meth...
function normalize_query (line 86) | def normalize_query(params):
function detect_ability (line 101) | def detect_ability(model: dict) -> int:
class VolcengineAdapter (line 112) | class VolcengineAdapter(OpenAIAdapter):
method __init__ (line 115) | def __init__(self, config: VolcengineConfig):
method auto_detect_models (line 118) | async def auto_detect_models(self) -> list[ModelConfig]:
FILE: kirara_ai/plugins/llm_preset_adapters/voyage_adapter.py
function resolve_media_base64 (line 17) | async def resolve_media_base64(inputs: list[LLMChatImageContent|LLMChatT...
class ReRankData (line 40) | class ReRankData(TypedDict):
class ReRankResponse (line 45) | class ReRankResponse(TypedDict):
class EmbeddingData (line 52) | class EmbeddingData(TypedDict):
class EmbeddingResponse (line 57) | class EmbeddingResponse(TypedDict):
class ModalEmbeddingResponse (line 63) | class ModalEmbeddingResponse(TypedDict):
class VoyageConfig (line 70) | class VoyageConfig(BaseModel):
class VoyageAdapter (line 75) | class VoyageAdapter(LLMBackendAdapter, LLMEmbeddingProtocol, LLMReRankPr...
method __init__ (line 78) | def __init__(self, config: VoyageConfig):
method embed (line 81) | def embed(self, req: LLMEmbeddingRequest) -> LLMEmbeddingResponse:
method _text_embedding (line 89) | def _text_embedding(self, req: LLMEmbeddingRequest) -> LLMEmbeddingRes...
method _multi_modal_embedding (line 122) | def _multi_modal_embedding(self, req: LLMEmbeddingRequest) -> LLMEmbed...
method rerank (line 168) | def rerank(self, req: LLMReRankRequest) -> LLMReRankResponse:
FILE: kirara_ai/tracing/core.py
class TraceRecord (line 19) | class TraceRecord(Base):
method update_from_event (line 30) | def update_from_event(self, event: TraceEvent) -> None:
method to_dict (line 34) | def to_dict(self) -> Dict[str, Any]:
method to_detail_dict (line 38) | def to_detail_dict(self) -> Dict[str, Any]:
function generate_trace_id (line 42) | def generate_trace_id() -> str:
class TracerBase (line 51) | class TracerBase(Generic[R], abc.ABC):
method __init__ (line 59) | def __init__(self, container: DependencyContainer, record_class: Type[...
method initialize (line 72) | def initialize(self):
method shutdown (line 78) | def shutdown(self):
method _register_event_handlers (line 92) | def _register_event_handlers(self):
method _unregister_event_handlers (line 96) | def _unregister_event_handlers(self):
method get_traces (line 99) | def get_traces(
method get_recent_traces (line 148) | def get_recent_traces(self, limit: int = 100) -> List[R]:
method get_trace_by_id (line 156) | def get_trace_by_id(self, trace_id: str) -> Optional[R]:
method save_trace_record (line 161) | def save_trace_record(self, record: R) -> Dict[str, Any]:
method update_trace_record (line 168) | def update_trace_record(self, trace_id: str, event: TraceEvent) -> Opt...
method register_ws_client (line 182) | def register_ws_client(self) -> Queue:
method unregister_ws_client (line 188) | def unregister_ws_client(self, queue: Queue):
method broadcast_ws_message (line 193) | def broadcast_ws_message(self, message: Dict):
FILE: kirara_ai/tracing/decorator.py
function trace_llm_chat (line 9) | def trace_llm_chat(func: Callable):
FILE: kirara_ai/tracing/llm_tracer.py
class LLMTracer (line 37) | class LLMTracer(TracerBase[LLMRequestTrace]):
method __init__ (line 44) | def __init__(self, container: DependencyContainer):
method initialize (line 48) | def initialize(self):
method _mark_pending_as_failed (line 60) | def _mark_pending_as_failed(self) -> int:
method _clean_old_traces (line 72) | def _clean_old_traces(self, days: int = 30) -> int:
method _register_event_handlers (line 82) | def _register_event_handlers(self):
method _unregister_event_handlers (line 88) | def _unregister_event_handlers(self):
method start_request_tracking (line 94) | def start_request_tracking(
method complete_request_tracking (line 116) | def complete_request_tracking(
method fail_request_tracking (line 142) | def fail_request_tracking(
method _on_request_start (line 170) | def _on_request_start(self, event: LLMRequestStartEvent):
method _on_request_complete (line 189) | def _on_request_complete(self, event: LLMRequestCompleteEvent):
method _on_request_fail (line 202) | def _on_request_fail(self, event: LLMRequestFailEvent):
method get_statistics (line 219) | def get_statistics(self) -> Dict:
FILE: kirara_ai/tracing/manager.py
class TracingManager (line 14) | class TracingManager:
method __init__ (line 18) | def __init__(self, container: DependencyContainer, database_manager: D...
method initialize (line 25) | def initialize(self):
method shutdown (line 36) | def shutdown(self):
method register_tracer (line 46) | def register_tracer(self, name: str, tracer: TracerBase):
method get_tracer (line 52) | def get_tracer(self, name: str) -> Optional[TracerBase]:
method get_all_tracers (line 56) | def get_all_tracers(self) -> Dict[str, TracerBase]:
method get_tracer_types (line 60) | def get_tracer_types(self) -> List[str]:
method publish_event (line 64) | def publish_event(self, event: TraceEvent):
method register_ws_client (line 69) | def register_ws_client(self, tracer_name: str) -> asyncio.Queue:
method unregister_ws_client (line 75) | def unregister_ws_client(self, tracer_name: str, queue: asyncio.Queue):
method get_recent_traces (line 83) | def get_recent_traces(self, tracer_name: str, limit: int = 100) -> Lis...
method get_trace_by_id (line 90) | def get_trace_by_id(self, tracer_name: str, trace_id: str) -> Optional...
FILE: kirara_ai/tracing/models.py
class LLMRequestTrace (line 11) | class LLMRequestTrace(TraceRecord):
method __repr__ (line 47) | def __repr__(self):
method update_from_event (line 50) | def update_from_event(self, event: TraceEvent) -> None:
method to_dict (line 83) | def to_dict(self) -> Dict[str, Any]:
method to_detail_dict (line 101) | def to_detail_dict(self) -> Dict[str, Any]:
method request (line 109) | def request(self) -> Optional[Dict[str, Any]]:
method request (line 114) | def request(self, value: Any):
method response (line 120) | def response(self) -> Optional[Dict[str, Any]]:
method response (line 125) | def response(self, value: Any):
FILE: kirara_ai/web/api/block/diagnostics/base_diagnostic.py
class BaseDiagnostic (line 13) | class BaseDiagnostic(ABC):
method __init__ (line 18) | def __init__(self, ls: LanguageServer):
method check (line 22) | def check(self, doc: Document) -> List[Diagnostic]:
method get_code_actions (line 33) | def get_code_actions(self, params: CodeActionParams, relevant_diagnost...
method _create_diagnostic (line 47) | def _create_diagnostic(self, message: str, node: Optional[ast.AST], se...
method _ast_node_to_string (line 93) | def _ast_node_to_string(self, node: Optional[ast.AST]) -> str:
FILE: kirara_ai/web/api/block/diagnostics/import_check.py
class ImportDiagnostic (line 16) | class ImportDiagnostic(BaseDiagnostic):
method _get_package_context (line 21) | def _get_package_context(self, path: Optional[str]) -> Tuple[Optional[...
method check (line 43) | def check(self, doc: Document) -> List[Diagnostic]:
method get_code_actions (line 168) | def get_code_actions(self, params: CodeActionParams, relevant_diagnost...
FILE: kirara_ai/web/api/block/diagnostics/jedi_syntax_check.py
class JediSyntaxErrorDiagnostic (line 14) | class JediSyntaxErrorDiagnostic(BaseDiagnostic):
method __init__ (line 19) | def __init__(self, ls: LanguageServer):
method check (line 22) | def check(self, doc: Document) -> List[Diagnostic]:
FILE: kirara_ai/web/api/block/diagnostics/mandatory_function.py
class MandatoryFunctionDiagnostic (line 15) | class MandatoryFunctionDiagnostic(BaseDiagnostic):
method __init__ (line 28) | def __init__(self, ls: LanguageServer, config: Optional[Dict[str, Any]]):
method update_config (line 42) | def update_config(self, config: Dict[str, Any]) -> None:
method check (line 72) | def check(self, doc: Document) -> List[Diagnostic]:
method get_code_actions (line 212) | def get_code_actions(self, params: CodeActionParams, relevant_diagnost...
FILE: kirara_ai/web/api/block/diagnostics/pyflakes_check.py
class _LspReporter (line 17) | class _LspReporter(pyflakes_reporter.Reporter):
method __init__ (line 18) | def __init__(self, source_name: str):
method unexpectedError (line 23) | def unexpectedError(self, filename: str, msg: str):
method syntaxError (line 35) | def syntaxError(self, filename: str, msg: str, lineno: int, offset: in...
method flake (line 61) | def flake(self, message: Any):
class PyflakesDiagnostic (line 129) | class PyflakesDiagnostic(BaseDiagnostic):
method __init__ (line 134) | def __init__(self, ls: LanguageServer):
method check (line 137) | def check(self, doc: Document) -> List[Diagnostic]:
method get_code_actions (line 164) | def get_code_actions(self, params: CodeActionParams, relevant_diagnost...
FILE: kirara_ai/web/api/block/models.py
class BlockType (line 8) | class BlockType(BaseModel):
class BlockTypeList (line 20) | class BlockTypeList(BaseModel):
class BlockTypeResponse (line 26) | class BlockTypeResponse(BaseModel):
FILE: kirara_ai/web/api/block/python_lsp.py
class QuartWsTransport (line 29) | class QuartWsTransport(asyncio.Transport):
method __init__ (line 30) | def __init__(self, queue: asyncio.Queue):
method write (line 33) | def write(self, message: str):
method close (line 41) | def close(self):
class PythonLanguageServer (line 45) | class PythonLanguageServer(LanguageServer):
method __init__ (line 48) | def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None):
method configure_mandatory_function_checker (line 60) | def configure_mandatory_function_checker(self, config: Dict[str, Any])...
method _setup_handlers (line 93) | def _setup_handlers(self):
method _get_script (line 181) | def _get_script(self, params: Union[TextDocumentPositionParams, Comple...
method _get_completions (line 206) | def _get_completions(self, params: CompletionParams) -> CompletionList:
method _get_hover (line 240) | def _get_hover(self, params: HoverParams) -> Optional[Hover]:
method _get_signature_help (line 279) | def _get_signature_help(self, params: SignatureHelpParams) -> Optional...
method _get_definition (line 334) | def _get_definition(self, params: DefinitionParams) -> List[Location]:
method _get_document_symbols (line 372) | def _get_document_symbols(self, params: DocumentSymbolParams) -> List[...
method _map_completion_type (line 423) | def _map_completion_type(self, type_str: str) -> CompletionItemKind:
method _map_symbol_type (line 443) | def _map_symbol_type(self, type_str: str) -> SymbolKind:
method _publish_diagnostics (line 464) | def _publish_diagnostics(self, ls: LanguageServer, doc_uri: str):
method _get_code_actions (line 492) | def _get_code_actions(self, params: CodeActionParams) -> Optional[List...
FILE: kirara_ai/web/api/block/routes.py
function list_block_types (line 20) | async def list_block_types() -> Any:
function get_block_type (line 52) | async def get_block_type(type_name: str) -> Any:
function get_type_compatibility (line 82) | async def get_type_compatibility() -> Any:
function code_lsp (line 88) | async def code_lsp():
FILE: kirara_ai/web/api/dispatch/models.py
class DispatchRuleList (line 8) | class DispatchRuleList(BaseModel):
class DispatchRuleResponse (line 14) | class DispatchRuleResponse(BaseModel):
FILE: kirara_ai/web/api/dispatch/routes.py
function list_rules (line 14) | async def list_rules():
function get_rule (line 25) | async def get_rule(rule_id: str):
function create_rule (line 38) | async def create_rule():
function update_rule (line 68) | async def update_rule(rule_id: str):
function delete_rule (line 100) | async def delete_rule(rule_id: str):
function enable_rule (line 119) | async def enable_rule(rule_id: str):
function disable_rule (line 141) | async def disable_rule(rule_id: str):
function get_rule_types (line 163) | async def get_rule_types():
function get_rule_config_schema (line 170) | async def get_rule_config_schema(rule_type: str):
FILE: kirara_ai/web/api/im/models.py
class IMAdapterStatus (line 12) | class IMAdapterStatus(IMAdapterConfig):
class IMAdapterList (line 18) | class IMAdapterList(BaseModel):
class IMAdapterResponse (line 24) | class IMAdapterResponse(BaseModel):
class IMAdapterTypes (line 30) | class IMAdapterTypes(BaseModel):
class IMAdapterConfigSchema (line 36) | class IMAdapterConfigSchema(BaseModel):
FILE: kirara_ai/web/api/im/routes.py
function _create_adapter (line 20) | def _create_adapter(manager: IMManager, name: str, adapter: str, config:...
function get_adapter_types (line 30) | async def get_adapter_types():
function list_adapters (line 40) | async def list_adapters():
function get_adapter (line 59) | async def get_adapter(adapter_id: str):
function create_adapter (line 86) | async def create_adapter():
function update_adapter (line 128) | async def update_adapter(adapter_id: str):
function delete_adapter (line 203) | async def delete_adapter(adapter_id: str):
function start_adapter (line 224) | async def start_adapter(adapter_id: str):
function stop_adapter (line 241) | async def stop_adapter(adapter_id: str):
function get_adapter_config_schema (line 258) | async def get_adapter_config_schema(adapter_type: str):
FILE: kirara_ai/web/api/llm/models.py
class LLMBackendInfo (line 8) | class LLMBackendInfo(LLMBackendConfig):
class LLMBackendList (line 13) | class LLMBackendList(BaseModel):
class LLMBackendResponse (line 19) | class LLMBackendResponse(BaseModel):
class LLMBackendListResponse (line 26) | class LLMBackendListResponse(BaseModel):
class LLMBackendCreateRequest (line 33) | class LLMBackendCreateRequest(LLMBackendConfig):
class LLMBackendUpdateRequest (line 38) | class LLMBackendUpdateRequest(LLMBackendConfig):
class LLMAdapterTypes (line 43) | class LLMAdapterTypes(BaseModel):
class LLMAdapterConfigSchema (line 49) | class LLMAdapterConfigSchema(BaseModel):
class ModelConfigListResponse (line 56) | class ModelConfigListResponse(BaseModel):
FILE: kirara_ai/web/api/llm/routes.py
function get_adapter_types (line 22) | async def get_adapter_types():
function list_backends (line 30) | async def list_backends():
function get_backend (line 55) | async def get_backend(backend_name: str):
function create_backend (line 81) | async def create_backend():
function update_backend (line 122) | async def update_backend(backend_name: str):
function delete_backend (line 171) | async def delete_backend(backend_name: str):
function get_adapter_config_schema (line 215) | async def get_adapter_config_schema(adapter_type: str):
function supports_auto_detect_models (line 232) | async def supports_auto_detect_models(adapter_type: str):
function auto_detect_models (line 256) | async def auto_detect_models(backend_name: str):
FILE: kirara_ai/web/api/mcp/models.py
class MCPServerInfo (line 6) | class MCPServerInfo(BaseModel):
class MCPToolInfo (line 16) | class MCPToolInfo(BaseModel):
class MCPServerList (line 22) | class MCPServerList(BaseModel):
class MCPStatistics (line 31) | class MCPStatistics(BaseModel):
class MCPServerCreateRequest (line 42) | class MCPServerCreateRequest(BaseModel):
class MCPServerUpdateRequest (line 51) | class MCPServerUpdateRequest(BaseModel):
FILE: kirara_ai/web/api/mcp/routes.py
function _convert_to_server_info (line 23) | def _convert_to_server_info(server: MCPServer) -> MCPServerInfo:
function list_servers (line 39) | async def list_servers():
function get_statistics (line 99) | async def get_statistics():
function get_server (line 129) | async def get_server(server_id: str):
function get_server_tools (line 152) | async def get_server_tools(server_id: str):
function check_server_id (line 189) | async def check_server_id(server_id: str):
function create_server (line 209) | async def create_server():
function update_server (line 255) | async def update_server(server_id: str):
function delete_server (line 332) | async def delete_server(server_id: str):
function start_server (line 371) | async def start_server(server_id: str):
function stop_server (line 392) | async def stop_server(server_id: str):
function get_all_tools (line 413) | async def get_all_tools():
function call_tool (line 439) | async def call_tool(server_id: str):
function get_server_prompts (line 465) | async def get_server_prompts(server_id: str):
function get_server_resources (line 486) | async def get_server_resources(server_id: str):
FILE: kirara_ai/web/api/media/models.py
class MediaMetadata (line 7) | class MediaMetadata(BaseModel):
class MediaItem (line 18) | class MediaItem(BaseModel):
class MediaListResponse (line 26) | class MediaListResponse(BaseModel):
class MediaSearchParams (line 34) | class MediaSearchParams(BaseModel):
class MediaBatchDeleteRequest (line 45) | class MediaBatchDeleteRequest(BaseModel):
FILE: kirara_ai/web/api/media/routes.py
function generate_thumbnail (line 26) | async def generate_thumbnail(image_data: bytes) -> io.BytesIO:
function _get_media_manager (line 50) | def _get_media_manager() -> MediaManager:
function _convert_media_to_api_item (line 54) | def _convert_media_to_api_item(media: Media) -> Optional[MediaItem]:
function list_media (line 79) | async def list_media():
function get_media_file (line 161) | async def get_media_file(media_id):
function get_thumbnail (line 172) | async def get_thumbnail(media_id):
function delete_media (line 198) | async def delete_media(media_id):
function batch_delete (line 213) | async def batch_delete():
function get_system_info (line 231) | async def get_system_info():
function set_config (line 272) | async def set_config():
function cleanup_unreferenced (line 285) | async def cleanup_unreferenced():
FILE: kirara_ai/web/api/plugin/models.py
class PluginList (line 8) | class PluginList(BaseModel):
class PluginResponse (line 14) | class PluginResponse(BaseModel):
class InstallPluginRequest (line 20) | class InstallPluginRequest(BaseModel):
FILE: kirara_ai/web/api/plugin/routes.py
function get_meta_params (line 21) | def get_meta_params() -> dict:
function is_upgradable (line 28) | def is_upgradable(installed_version: str, market_version: str) -> bool:
function fetch_from_market (line 36) | async def fetch_from_market(path: str, params: dict | None = None) -> dict:
function enrich_plugin_data (line 50) | async def enrich_plugin_data(plugins: list, loader: PluginLoader) -> list:
function search_plugins (line 77) | async def search_plugins():
function get_market_plugin_info (line 99) | async def get_market_plugin_info(plugin_name: str):
function list_plugins (line 115) | async def list_plugins():
function get_plugin_details (line 124) | async def get_plugin_details(plugin_name: str):
function install_plugin (line 137) | async def install_plugin():
function uninstall_plugin (line 168) | async def uninstall_plugin(plugin_name: str):
function enable_plugin (line 198) | async def enable_plugin(plugin_name: str):
function disable_plugin (line 228) | async def disable_plugin(plugin_name: str):
function update_plugin (line 264) | async def update_plugin(plugin_name: str):
FILE: kirara_ai/web/api/system/models.py
class SystemStatus (line 6) | class SystemStatus(BaseModel):
class SystemStatusResponse (line 24) | class SystemStatusResponse(BaseModel):
class UpdateStatus (line 30) | class UpdateStatus(BaseModel):
class UpdateCheckResponse (line 35) | class UpdateCheckResponse(BaseModel):
FILE: kirara_ai/web/api/system/routes.py
function logs_websocket (line 38) | async def logs_websocket():
function get_system_config (line 66) | async def get_system_config():
function update_web_config (line 94) | async def update_web_config():
function update_plugins_config (line 111) | async def update_plugins_config():
function update_registry_config (line 127) | async def update_registry_config():
function update_system_config (line 147) | async def update_system_config():
function update_tracing_config (line 173) | async def update_tracing_config():
function get_system_status (line 190) | async def get_system_status():
function check_update (line 251) | async def check_update():
function perform_update (line 277) | async def perform_update():
function restart_system (line 316) | async def restart_system():
FILE: kirara_ai/web/api/system/utils.py
function get_installed_version (line 11) | def get_installed_version() -> str:
function get_latest_pypi_version (line 26) | async def get_latest_pypi_version(package_name: str) -> tuple[str, str]:
function get_latest_npm_version (line 43) | async def get_latest_npm_version(package_name: str, registry: str = "htt...
function download_file (line 58) | async def download_file(url: str, temp_dir: str) -> tuple[str, str]:
function get_cpu_info (line 84) | def get_cpu_info() -> str:
function get_memory_usage (line 104) | def get_memory_usage() -> dict:
function get_cpu_usage (line 117) | def get_cpu_usage() -> float:
FILE: kirara_ai/web/api/tracing/routes.py
function get_trace_types (line 21) | async def get_trace_types():
function get_llm_traces (line 33) | async def get_llm_traces():
function get_llm_trace_detail (line 77) | async def get_llm_trace_detail(trace_id: str):
function get_llm_statistics (line 95) | async def get_llm_statistics():
function tracing_ws (line 109) | async def tracing_ws():
FILE: kirara_ai/web/api/workflow/models.py
class Wire (line 8) | class Wire(BaseModel):
class BlockInstance (line 17) | class BlockInstance(BaseModel):
class WorkflowDefinition (line 26) | class WorkflowDefinition(BaseModel):
class WorkflowInfo (line 39) | class WorkflowInfo(BaseModel):
class WorkflowList (line 50) | class WorkflowList(BaseModel):
class WorkflowResponse (line 56) | class WorkflowResponse(BaseModel):
FILE: kirara_ai/web/api/workflow/routes.py
function list_workflows (line 18) | async def list_workflows():
function get_workflow (line 43) | async def get_workflow(group_id: str, workflow_id: str):
function create_workflow (line 93) | async def create_workflow(group_id: str, workflow_id: str):
function update_workflow (line 151) | async def update_workflow(group_id: str, workflow_id: str):
function delete_workflow (line 216) | async def delete_workflow(group_id: str, workflow_id: str):
FILE: kirara_ai/web/app.py
function create_web_api_app (line 69) | def create_web_api_app(container: DependencyContainer) -> Quart:
function create_app (line 107) | def create_app(container: DependencyContainer) -> FastAPI:
class WebServer (line 209) | class WebServer:
method __init__ (line 216) | def __init__(self, container: DependencyContainer):
method mount_app (line 257) | def mount_app(self, prefix: str, app):
method _check_port_available (line 261) | def _check_port_available(self, host: str, port: int) -> bool:
method start (line 271) | async def start(self):
method stop (line 297) | async def stop(self):
method add_static_assets (line 311) | def add_static_assets(self, url_path: str, local_path: str):
method _check_and_install_webui (line 319) | def _check_and_install_webui(self):
method _install_webui (line 327) | async def _install_webui(self):
FILE: kirara_ai/web/auth/middleware.py
function require_auth (line 8) | def require_auth(f):
FILE: kirara_ai/web/auth/models.py
class LoginRequest (line 4) | class LoginRequest(BaseModel):
class ChangePasswordRequest (line 8) | class ChangePasswordRequest(BaseModel):
class TokenResponse (line 13) | class TokenResponse(BaseModel):
FILE: kirara_ai/web/auth/routes.py
function login (line 17) | async def login():
function change_password (line 37) | async def change_password():
function check_first_time (line 57) | async def check_first_time():
FILE: kirara_ai/web/auth/services.py
class AuthService (line 7) | class AuthService(ABC):
method is_first_time (line 9) | def is_first_time(self) -> bool:
method save_password (line 13) | def save_password(self, password: str) -> None:
method verify_password (line 17) | def verify_password(self, password: str) -> bool:
method create_access_token (line 21) | def create_access_token(self, expires_delta: Optional[timedelta] = Non...
method verify_token (line 25) | def verify_token(self, token: str) -> bool:
class FileBasedAuthService (line 29) | class FileBasedAuthService(AuthService):
method __init__ (line 30) | def __init__(self, password_file: Path, secret_key: str):
method is_first_time (line 34) | def is_first_time(self) -> bool:
method save_password (line 37) | def save_password(self, password: str) -> None:
method verify_password (line 45) | def verify_password(self, password: str) -> bool:
method create_access_token (line 55) | def create_access_token(self, expires_delta: Optional[timedelta] = Non...
method verify_token (line 60) | def verify_token(self, token: str) -> bool:
class MockAuthService (line 66) | class MockAuthService(AuthService):
method __init__ (line 67) | def __init__(self):
method is_first_time (line 71) | def is_first_time(self) -> bool:
method save_password (line 74) | def save_password(self, password: str) -> None:
method verify_password (line 78) | def verify_password(self, password: str) -> bool:
method create_access_token (line 81) | def create_access_token(self, expires_delta: Optional[timedelta] = Non...
method verify_token (line 84) | def verify_token(self, token: str) -> bool:
FILE: kirara_ai/web/auth/utils.py
function hash_password (line 8) | def hash_password(password: str) -> bytes:
function verify_password (line 13) | def verify_password(password: str, hashed: bytes) -> bool:
function create_jwt_token (line 17) | def create_jwt_token(secret_key: str, expires_delta: Optional[timedelta]...
function verify_jwt_token (line 28) | def verify_jwt_token(token: str, secret_key: str) -> bool:
FILE: kirara_ai/web/utils.py
function create_no_cache_response (line 17) | async def create_no_cache_response(file_path: Path, request: Request) ->...
function test_npm_registry_speed (line 34) | async def test_npm_registry_speed(registries: list[str]) -> str:
function install_webui (line 83) | async def install_webui(install_path: Path) -> tuple[bool, str]:
FILE: kirara_ai/workflow/core/block/base.py
class Block (line 7) | class Block:
method __init__ (line 21) | def __init__(
method execute (line 35) | def execute(self, **kwargs) -> Dict[str, Any]:
class ConditionBlock (line 40) | class ConditionBlock(Block):
method __init__ (line 48) | def __init__(
method execute (line 57) | def execute(self, **kwargs) -> Dict[str, Any]:
class LoopBlock (line 62) | class LoopBlock(Block):
method __init__ (line 71) | def __init__(
method execute (line 83) | def execute(self, **kwargs) -> Dict[str, Any]:
class LoopEndBlock (line 92) | class LoopEndBlock(Block):
method __init__ (line 100) | def __init__(self, inputs: Dict[str, "Input"]):
method execute (line 105) | def execute(self, **kwargs) -> Dict[str, Any]:
FILE: kirara_ai/workflow/core/block/input_output.py
class Input (line 4) | class Input:
method __init__ (line 5) | def __init__(
method validate (line 21) | def validate(self, value: Any) -> bool:
class Output (line 27) | class Output:
method __init__ (line 28) | def __init__(self, name: str, label: str, data_type: type, description...
method validate (line 34) | def validate(self, value: Any) -> bool:
FILE: kirara_ai/workflow/core/block/param.py
class ParamMeta (line 9) | class ParamMeta:
method __init__ (line 10) | def __init__(self, label: Optional[str] = None, description: Optional[...
method __repr__ (line 15) | def __repr__(self):
method __str__ (line 18) | def __str__(self):
FILE: kirara_ai/workflow/core/block/registry.py
function extract_block_param (line 12) | def extract_block_param(param: Parameter, type_system: TypeSystem) -> Bl...
class BlockRegistry (line 66) | class BlockRegistry:
method __init__ (line 69) | def __init__(self):
method register (line 74) | def register(
method get (line 104) | def get(self, full_name: str) -> Optional[Type[Block]]:
method get_localized_name (line 108) | def get_localized_name(self, block_id: str) -> Optional[str]:
method clear (line 112) | def clear(self):
method get_block_type_name (line 117) | def get_block_type_name(self, block_class: Type[Block]) -> str:
method get_all_types (line 130) | def get_all_types(self) -> List[Type[Block]]:
method extract_block_info (line 134) | def extract_block_info(
method get_builtin_params (line 189) | def get_builtin_params(self) -> List[str]:
method get_type_compatibility_map (line 194) | def get_type_compatibility_map(self) -> Dict[str, Dict[str, bool]]:
method is_type_compatible (line 198) | def is_type_compatible(self, source_type: str, target_type: str) -> bool:
FILE: kirara_ai/workflow/core/block/schema.py
class BlockInput (line 8) | class BlockInput(BaseModel):
class BlockOutput (line 19) | class BlockOutput(BaseModel):
class BlockConfig (line 28) | class BlockConfig(BaseModel):
FILE: kirara_ai/workflow/core/block/type_system.py
class TypeSystem (line 5) | class TypeSystem:
method __init__ (line 8) | def __init__(self) -> None:
method register_type (line 12) | def register_type(self, type_name: str, type_class: Type):
method get_type (line 16) | def get_type(self, type_name: str) -> Optional[Type]:
method get_type_name (line 20) | def get_type_name(self, type_obj: Type) -> str:
method extract_type_info (line 27) | def extract_type_info(self, param: Parameter) -> tuple[str, bool, Any]...
method extract_type_info (line 30) | def extract_type_info(self, param: Type) -> tuple[str, bool, Any]: ...
method extract_type_info (line 32) | def extract_type_info(self, param: Union[Parameter, Type]) -> tuple[st...
method is_compatible (line 76) | def is_compatible(self, source_type: str, target_type: str) -> bool:
method get_compatibility_map (line 109) | def get_compatibility_map(self) -> Dict[str, Dict[str, bool]]:
FILE: kirara_ai/workflow/core/dispatch/dispatcher.py
class WorkflowDispatcher (line 16) | class WorkflowDispatcher:
method __init__ (line 19) | def __init__(self, container: DependencyContainer):
method register_rule (line 27) | def register_rule(self, rule: CombinedDispatchRule):
method dispatch (line 32) | async def dispatch(self, source: IMAdapter, message: IMMessage):
FILE: kirara_ai/workflow/core/dispatch/exceptions.py
class WorkflowNotFoundException (line 1) | class WorkflowNotFoundException(Exception):
method __init__ (line 4) | def __init__(self, message: str):
FILE: kirara_ai/workflow/core/dispatch/models/dispatch_rules.py
class SimpleDispatchRule (line 13) | class SimpleDispatchRule(BaseModel):
class RuleGroup (line 18) | class RuleGroup(BaseModel):
class CombinedDispatchRule (line 23) | class CombinedDispatchRule(BaseModel):
method match (line 34) | def match(self, message: IMMessage, workflow_registry: WorkflowRegistr...
method get_workflow (line 83) | def get_workflow(self, container: DependencyContainer) -> Optional[Wor...
FILE: kirara_ai/workflow/core/dispatch/registry.py
class DispatchRuleRegistry (line 17) | class DispatchRuleRegistry:
method __init__ (line 20) | def __init__(self, container: DependencyContainer):
method register (line 27) | def register(self, rule: CombinedDispatchRule):
method get_rule (line 34) | def get_rule(self, rule_id: str) -> Optional[CombinedDispatchRule]:
method get_all_rules (line 38) | def get_all_rules(self) -> List[CombinedDispatchRule]:
method get_active_rules (line 42) | def get_active_rules(self) -> List[CombinedDispatchRule]:
method create_rule (line 47) | def create_rule(self, rule: CombinedDispatchRule) -> CombinedDispatchR...
method update_rule (line 58) | def update_rule(
method delete_rule (line 69) | def delete_rule(self, rule_id: str):
method enable_rule (line 75) | def enable_rule(self, rule_id: str):
method disable_rule (line 82) | def disable_rule(self, rule_id: str):
method _convert_old_rule (line 89) | def _convert_old_rule(self, rule_data: Dict[str, Any]) -> CombinedDisp...
method load_rules (line 116) | def load_rules(self, rules_dir: Optional[str] = None):
method save_rules (line 158) | def save_rules(self, rules_dir: Optional[str] = None):
FILE: kirara_ai/workflow/core/dispatch/rules/base.py
class RuleConfig (line 12) | class RuleConfig(BaseModel):
class DispatchRule (line 15) | class DispatchRule(ABC):
method __init__ (line 25) | def __init__(self, workflow_registry: WorkflowRegistry, workflow_id: s...
method match (line 37) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_workflow (line 40) | def get_workflow(self, container: DependencyContainer) -> Workflow:
method register_rule_type (line 47) | def register_rule_type(cls, rule_class: Type["DispatchRule"]):
method get_rule_type (line 52) | def get_rule_type(cls, type_name: str) -> Type["DispatchRule"]:
method get_config (line 59) | def get_config(self) -> RuleConfig:
method from_config (line 64) | def from_config(
method __str__ (line 69) | def __str__(self) -> str:
FILE: kirara_ai/workflow/core/dispatch/rules/message_rules.py
class RegexRuleConfig (line 14) | class RegexRuleConfig(RuleConfig):
class RegexMatchRule (line 18) | class RegexMatchRule(DispatchRule):
method __init__ (line 23) | def __init__(self, pattern: str, workflow_registry: WorkflowRegistry, ...
method match (line 27) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 30) | def get_config(self) -> RegexRuleConfig:
method from_config (line 34) | def from_config(cls, config: RegexRuleConfig, workflow_registry: Workf...
class PrefixRuleConfig (line 37) | class PrefixRuleConfig(RuleConfig):
class PrefixMatchRule (line 41) | class PrefixMatchRule(DispatchRule):
method __init__ (line 46) | def __init__(self, prefix: str, workflow_registry: WorkflowRegistry, w...
method match (line 50) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 60) | def get_config(self) -> PrefixRuleConfig:
method from_config (line 64) | def from_config(cls, config: PrefixRuleConfig, workflow_registry: Work...
class KeywordRuleConfig (line 68) | class KeywordRuleConfig(RuleConfig):
class KeywordMatchRule (line 72) | class KeywordMatchRule(DispatchRule):
method __init__ (line 77) | def __init__(self, keywords: list[str], workflow_registry: WorkflowReg...
method match (line 81) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 84) | def get_config(self) -> KeywordRuleConfig:
method from_config (line 88) | def from_config(cls, config: KeywordRuleConfig, workflow_registry: Wor...
class BotMentionMatchRule (line 91) | class BotMentionMatchRule(DispatchRule):
method __init__ (line 96) | def __init__(self, workflow_registry: WorkflowRegistry, workflow_id: s...
method match (line 99) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 103) | def get_config(self) -> RuleConfig:
method from_config (line 107) | def from_config(cls, config: RuleConfig, workflow_registry: WorkflowRe...
FILE: kirara_ai/workflow/core/dispatch/rules/sender_rules.py
class ChatSenderMatchRuleConfig (line 13) | class ChatSenderMatchRuleConfig(RuleConfig):
class ChatSenderMatchRule (line 20) | class ChatSenderMatchRule(DispatchRule):
method __init__ (line 25) | def __init__(
method match (line 35) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 47) | def get_config(self) -> ChatSenderMatchRuleConfig:
method from_config (line 53) | def from_config(
class ChatSenderMismatchRule (line 61) | class ChatSenderMismatchRule(DispatchRule):
method __init__ (line 66) | def __init__(
method match (line 77) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 89) | def get_config(self) -> ChatSenderMatchRuleConfig:
method from_config (line 95) | def from_config(
class ChatTypeMatchRuleConfig (line 104) | class ChatTypeMatchRuleConfig(RuleConfig):
class ChatTypeMatchRule (line 108) | class ChatTypeMatchRule(DispatchRule):
method __init__ (line 113) | def __init__(self, chat_type: ChatType, workflow_registry: WorkflowReg...
method match (line 117) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 120) | def get_config(self) -> ChatTypeMatchRuleConfig:
method from_config (line 124) | def from_config(cls, config: ChatTypeMatchRuleConfig, workflow_registr...
FILE: kirara_ai/workflow/core/dispatch/rules/system_rules.py
class RandomChanceRuleConfig (line 14) | class RandomChanceRuleConfig(RuleConfig):
class RandomChanceMatchRule (line 20) | class RandomChanceMatchRule(DispatchRule):
method __init__ (line 25) | def __init__(self, chance: int, workflow_registry: WorkflowRegistry, w...
method match (line 29) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 34) | def get_config(self) -> RandomChanceRuleConfig:
method from_config (line 38) | def from_config(
class IMInstanceMatchRuleConfig (line 43) | class IMInstanceMatchRuleConfig(RuleConfig):
class IMInstanceMatchRule (line 47) | class IMInstanceMatchRule(DispatchRule):
method __init__ (line 52) | def __init__(self, im_instance: str, workflow_registry: WorkflowRegist...
method match (line 56) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 61) | def get_config(self) -> IMInstanceMatchRuleConfig:
method from_config (line 65) | def from_config(
class FallbackMatchRule (line 71) | class FallbackMatchRule(DispatchRule):
method __init__ (line 76) | def __init__(self, workflow_registry: WorkflowRegistry, workflow_id: s...
method match (line 80) | def match(self, message: IMMessage, container: DependencyContainer) ->...
method get_config (line 83) | def get_config(self) -> RuleConfig:
method from_config (line 87) | def from_config(
FILE: kirara_ai/workflow/core/execution/exceptions.py
class BlockExecutionFailedException (line 1) | class BlockExecutionFailedException(Exception):
class WorkflowExecutionTimeoutException (line 4) | class WorkflowExecutionTimeoutException(Exception):
FILE: kirara_ai/workflow/core/execution/executor.py
class WorkflowExecutor (line 18) | class WorkflowExecutor:
method __init__ (line 21) | def __init__(self, container: DependencyContainer, workflow: Workflow,...
method _build_execution_graph (line 41) | def _build_execution_graph(self):
method run (line 70) | async def run(self) -> Dict[str, Any]:
method _execute_nodes (line 99) | async def _execute_nodes(self, blocks: List[Block], executor, loop):
method _execute_conditional_branch (line 112) | async def _execute_conditional_branch(self, block: ConditionBlock, exe...
method _execute_loop (line 135) | async def _execute_loop(self, block: LoopBlock, executor, loop):
method _execute_normal_block (line 162) | async def _execute_normal_block(self, block: Block, executor, loop):
method _can_execute (line 201) | def _can_execute(self, block: Block) -> bool:
method _gather_inputs (line 243) | def _gather_inputs(self, block: Block) -> Dict[str, Any]:
method set_variable (line 274) | def set_variable(self, name: str, value: Any) -> None:
method get_variable (line 283) | def get_variable(self, name: str, default: Any = None) -> Any:
FILE: kirara_ai/workflow/core/workflow/base.py
class WorkflowConfig (line 8) | class WorkflowConfig(BaseModel):
class Workflow (line 11) | class Workflow:
method __init__ (line 12) | def __init__(self, name: str, blocks: List["Block"], wires: List["Wire...
class Wire (line 20) | class Wire:
method __init__ (line 21) | def __init__(
method __repr__ (line 33) | def __repr__(self):
FILE: kirara_ai/workflow/core/workflow/builder.py
function get_block_class (line 17) | def get_block_class(type_name: str, registry: BlockRegistry) -> Type[Blo...
class BlockSpec (line 33) | class BlockSpec:
method __post_init__ (line 41) | def __post_init__(self):
class Node (line 47) | class Node:
method __init__ (line 59) | def __init__(
method ancestors (line 85) | def ancestors(self) -> List["Node"]:
class WorkflowBuilder (line 95) | class WorkflowBuilder:
method __init__ (line 176) | def __init__(self, name: str):
method _generate_unique_name (line 187) | def _generate_unique_name(self, base_name: str) -> str:
method _parse_block_spec (line 198) | def _parse_block_spec(self, block_spec: Union[Type[Block], tuple]) -> ...
method _get_available_inputs (line 221) | def _get_available_inputs(self, node: Node) -> List[str]:
method _find_matching_ports (line 227) | def _find_matching_ports(
method _store_wire_spec (line 253) | def _store_wire_spec(
method _create_node (line 277) | def _create_node(self, spec: BlockSpec, is_parallel: bool = False) -> ...
method use (line 298) | def use(
method chain (line 307) | def chain(
method parallel (line 322) | def parallel(
method condition (line 339) | def condition(self, condition_func: Callable) -> "WorkflowBuilder":
method if_then (line 345) | def if_then(
method else_then (line 368) | def else_then(self) -> "WorkflowBuilder":
method end_if (line 375) | def end_if(self) -> "WorkflowBuilder":
method loop (line 382) | def loop(
method end_loop (line 412) | def end_loop(self) -> "WorkflowBuilder":
method build (line 437) | def build(self, container: DependencyContainer) -> Workflow:
method set_config (line 472) | def set_config(self, config: WorkflowConfig):
method force_connect (line 476) | def force_connect(
method _find_parallel_nodes (line 486) | def _find_parallel_nodes(self, start_node: Node) -> List[Node]:
method update_position (line 499) | def update_position(self, name: str, position: Dict[str, int]):
method save_to_yaml (line 504) | def save_to_yaml(self, file_path: str, container: DependencyContainer):
method load_from_yaml (line 566) | def load_from_yaml(
FILE: kirara_ai/workflow/core/workflow/registry.py
class WorkflowRegistry (line 11) | class WorkflowRegistry:
method __init__ (line 16) | def __init__(self, container: DependencyContainer):
method get_workflow_path (line 22) | def get_workflow_path(cls, group_id: str, workflow_id: str) -> str:
method unregister (line 47) | def unregister(self, group_id: str, workflow_id: str):
method register (line 54) | def register(
method register_preset_workflow (line 65) | def register_preset_workflow(
method get_workflow (line 78) | def get_workflow(self, name: str, container: DependencyContainer) -> O...
method get (line 84) | def get(
method load_workflows (line 93) | def load_workflows(self, workflows_dir: Optional[str] = None):
FILE: kirara_ai/workflow/implementations/blocks/game/dice.py
class DiceRoll (line 11) | class DiceRoll(Block):
method execute (line 24) | def execute(self, message: IMMessage) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/game/gacha.py
class GachaSimulator (line 10) | class GachaSimulator(Block):
method __init__ (line 21) | def __init__(self, rates: Optional[Dict[str, float]] = None):
method _single_pull (line 25) | def _single_pull(self) -> str:
method execute (line 34) | def execute(self, message: IMMessage) -> Dict[str, IMMessage]:
FILE: kirara_ai/workflow/implementations/blocks/im/basic.py
class ExtractChatSender (line 10) | class ExtractChatSender(Block):
method execute (line 18) | def execute(self, **kwargs) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/im/messages.py
function im_adapter_options_provider (line 12) | def im_adapter_options_provider(container: DependencyContainer, block: B...
class GetIMMessage (line 15) | class GetIMMessage(Block):
method execute (line 25) | def execute(self, **kwargs) -> Dict[str, Any]:
class SendIMMessage (line 30) | class SendIMMessage(Block):
method __init__ (line 47) | def __init__(
method execute (line 52) | def execute(
class IMMessageToText (line 70) | class IMMessageToText(Block):
method execute (line 78) | def execute(self, msg: IMMessage) -> Dict[str, Any]:
class TextToIMMessage (line 83) | class TextToIMMessage(Block):
method __init__ (line 91) | def __init__(self, split_by: Annotated[Optional[str], ParamMeta(label=...
method execute (line 94) | def execute(self, text: str) -> Dict[str, Any]:
class AppendIMMessage (line 101) | class AppendIMMessage(Block):
method execute (line 112) | def execute(self, base_msg: IMMessage, append_msg: MessageElement) -> ...
FILE: kirara_ai/workflow/implementations/blocks/im/states.py
class ToggleEditState (line 11) | class ToggleEditState(Block):
method __init__ (line 19) | def __init__(
method execute (line 27) | def execute(self, sender: ChatSender) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/im/user_profile.py
class QueryUserProfileBlock (line 12) | class QueryUserProfileBlock(Block):
method __init__ (line 13) | def __init__(self, container: DependencyContainer):
method execute (line 26) | def execute(
FILE: kirara_ai/workflow/implementations/blocks/llm/basic.py
class LLMResponseToText (line 11) | class LLMResponseToText(Block):
method execute (line 19) | def execute(self, response: LLMChatResponse) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/llm/chat.py
function model_name_options_provider (line 21) | def model_name_options_provider(container: DependencyContainer, block: B...
class ChatMessageConstructor (line 26) | class ChatMessageConstructor(Block):
method substitute_variables (line 45) | def substitute_variables(self, text: str, executor: WorkflowExecutor) ...
method execute (line 81) | def execute(
class ChatCompletion (line 129) | class ChatCompletion(Block):
method __init__ (line 137) | def __init__(
method execute (line 150) | def execute(self, prompt: List[LLMChatMessage]) -> Dict[str, Any]:
class ChatResponseConverter (line 172) | class ChatResponseConverter(Block):
method execute (line 178) | def execute(self, resp: LLMChatResponse) -> Dict[str, Any]:
class ChatCompletionWithTools (line 194) | class ChatCompletionWithTools(Block):
method __init__ (line 210) | def __init__(self, model_name: Annotated[
method execute (line 227) | def execute(self, msg: List[LLMChatMessage], tools: List[Tool]) -> Dic...
FILE: kirara_ai/workflow/implementations/blocks/llm/image.py
class SimpleStableDiffusionWebUI (line 11) | class SimpleStableDiffusionWebUI(Block):
method __init__ (line 19) | def __init__(
method execute (line 40) | def execute(self, prompt: str, negative_prompt: str) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/mcp/tool.py
function get_enabled_mcp_tools (line 18) | def get_enabled_mcp_tools(container: DependencyContainer, block: Block) ...
class MCPToolProvider (line 23) | class MCPToolProvider(Block):
method __init__ (line 34) | def __init__(self, enabled_tools: Annotated[List[str], ParamMeta(label...
method _call_tool (line 38) | async def _call_tool(self, tool_call: ToolCall) -> LLMToolResultContent:
method execute (line 57) | def execute(self) -> Dict[str, Any]:
method _create_tool_result (line 81) | async def _create_tool_result(self, tool_id: str, tool_name: str, cont...
FILE: kirara_ai/workflow/implementations/blocks/memory/chat_memory.py
function scope_type_options_provider (line 14) | def scope_type_options_provider(container: DependencyContainer, block: B...
function decomposer_name_options_provider (line 18) | def decomposer_name_options_provider(container: DependencyContainer, blo...
class ChatMemoryQuery (line 22) | class ChatMemoryQuery(Block):
method __init__ (line 33) | def __init__(
method execute (line 63) | def execute(self, chat_sender: ChatSender) -> Dict[str, Any]:
class ChatMemoryStore (line 84) | class ChatMemoryStore(Block):
method __init__ (line 99) | def __init__(
method execute (line 121) | def execute(
FILE: kirara_ai/workflow/implementations/blocks/memory/clear_memory.py
class ClearMemory (line 11) | class ClearMemory(Block):
method __init__ (line 21) | def __init__(
method execute (line 29) | def execute(self, chat_sender: ChatSender) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/system/basic.py
class TextBlock (line 10) | class TextBlock(Block):
method __init__ (line 14) | def __init__(
method execute (line 19) | def execute(self) -> Dict[str, Any]:
class TextConcatBlock (line 24) | class TextConcatBlock(Block):
method execute (line 32) | def execute(self, text1: str, text2: str) -> Dict[str, Any]:
class TextReplaceBlock (line 37) | class TextReplaceBlock(Block):
method __init__ (line 45) | def __init__(
method execute (line 50) | def execute(self, text: str, new_text: Any) -> Dict[str, Any]:
class TextExtractByRegexBlock (line 57) | class TextExtractByRegexBlock(Block):
method __init__ (line 62) | def __init__(
method execute (line 67) | def execute(self, text: str) -> Dict[str, Any]:
class CurrentTimeBlock (line 79) | class CurrentTimeBlock(Block):
method execute (line 83) | def execute(self) -> Dict[str, Any]:
class CodeBlock (line 87) | class CodeBlock(Block):
method __init__ (line 92) | def __init__(self,
method execute (line 105) | def execute(self, **kwargs: Any) -> Dict[str, Any]: # 使用 Any 兼容各种输入类型
FILE: kirara_ai/workflow/implementations/blocks/system/help.py
function _format_rule_condition (line 11) | def _format_rule_condition(rule_type: str, config: Dict[str, Any]) -> str:
function _format_rule_group (line 29) | def _format_rule_group(group: RuleGroup) -> str:
class GenerateHelp (line 41) | class GenerateHelp(Block):
method execute (line 49) | def execute(self) -> Dict[str, Any]:
FILE: kirara_ai/workflow/implementations/blocks/system_blocks.py
function register_system_blocks (line 20) | def register_system_blocks(registry: BlockRegistry):
FILE: kirara_ai/workflow/implementations/blocks/variables/variable_blocks.py
class SetVariableBlock (line 11) | class SetVariableBlock(Block):
method __init__ (line 12) | def __init__(self, container: DependencyContainer):
method execute (line 21) | def execute(self, name: str, value: Any) -> Dict[str, Any]:
class GetVariableBlock (line 27) | class GetVariableBlock(Block):
method __init__ (line 28) | def __init__(self, container: DependencyContainer, var_type: Type[T]):
method execute (line 38) | def execute(self, name: str, default: Optional[T] = None) -> Dict[str,...
FILE: kirara_ai/workflow/implementations/factories/default_factory.py
class DefaultWorkflowFactory (line 11) | class DefaultWorkflowFactory:
method create_default_workflow (line 17) | def create_default_workflow() -> WorkflowBuilder:
FILE: kirara_ai/workflow/implementations/factories/game_factory.py
class GameWorkflowFactory (line 7) | class GameWorkflowFactory:
method create_dice_workflow (line 11) | def create_dice_workflow() -> WorkflowBuilder:
method create_gacha_workflow (line 21) | def create_gacha_workflow() -> WorkflowBuilder:
FILE: kirara_ai/workflow/implementations/factories/system_factory.py
class SystemWorkflowFactory (line 7) | class SystemWorkflowFactory:
method create_help_workflow (line 11) | def create_help_workflow() -> WorkflowBuilder:
method create_clear_memory_workflow (line 16) | def create_clear_memory_workflow() -> WorkflowBuilder:
FILE: kirara_ai/workflow/implementations/workflows/system_workflows.py
function register_system_workflows (line 7) | def register_system_workflows(registry: WorkflowRegistry):
FILE: tests/llm_adapters/conftest.py
function container (line 8) | def container():
function mock_endpoint (line 12) | def mock_endpoint():
function mock_endpoint_test_client (line 40) | def mock_endpoint_test_client():
class MockMedia (line 53) | class MockMedia(MagicMock):
method get_base64 (line 54) | async def get_base64(self) -> str:
method get_url (line 57) | async def get_url(self) -> str:
method description (line 61) | def description(self) -> str:
function mock_media_manager (line 65) | def mock_media_manager():
class MockTracer (line 76) | class MockTracer(MagicMock):
method start_request_tracking (line 77) | def start_request_tracking(self, *_) -> str:
method fail_request_tracking (line 80) | def fail_request_tracking(self, *_) -> None:
method complete_request_tracking (line 83) | def complete_request_tracking(self, *_) -> None:
function mock_tracer (line 87) | def mock_tracer() -> MockTracer:
FILE: tests/llm_adapters/mock_app/app.py
function default_authenticate (line 12) | def default_authenticate(authorization: str = Header(...)) -> None:
FILE: tests/llm_adapters/mock_app/gemini.py
function gemini_authenticate (line 8) | async def gemini_authenticate(key: str = Query(...)) -> None:
function chat (line 15) | async def chat(model: str, request:ChatRequest = Body()) -> dict:
function batch_embed_contents (line 40) | async def batch_embed_contents(model: str, _: BatchEmbeddingRequest = Bo...
FILE: tests/llm_adapters/mock_app/models/gemini.py
class BatchEmbeddingPart (line 8) | class BatchEmbeddingPart(BaseModel):
class BatchEmbeddingParts (line 10) | class BatchEmbeddingParts(BaseModel):
class BatchEmbeddingPayload (line 12) | class BatchEmbeddingPayload(BaseModel):
class BatchEmbeddingRequest (line 16) | class BatchEmbeddingRequest(BaseModel):
class Blob (line 19) | class Blob(BaseModel):
class FunctionCall (line 23) | class FunctionCall(BaseModel):
method regex_validator (line 30) | def regex_validator(cls, value: str) -> str:
class FunctionResponse (line 35) | class FunctionResponse(BaseModel):
method regex_validator (line 42) | def regex_validator(cls, value: str) -> str:
class FileData (line 47) | class FileData(BaseModel):
class ExecutableCode (line 51) | class ExecutableCode(BaseModel):
class CodeExecutionResult (line 55) | class CodeExecutionResult(BaseModel):
class Part (line 59) | class Part(BaseModel):
method validate_mutually_exclusive_fields (line 73) | def validate_mutually_exclusive_fields(self) -> Self:
class Content (line 89) | class Content(BaseModel):
class FunctionDeclaration (line 93) | class FunctionDeclaration(BaseModel):
method regex_validator (line 101) | def regex_validator(cls, value: str) -> str:
class DynamicRetrievalConfig (line 105) | class DynamicRetrievalConfig(BaseModel):
class GoogleSearchRetrieval (line 109) | class GoogleSearchRetrieval(BaseModel):
class Tool (line 112) | class Tool(BaseModel):
class FunctionCallingConfig (line 118) | class FunctionCallingConfig(BaseModel):
class ToolConfig (line 122) | class ToolConfig(BaseModel):
class SafetySettings (line 125) | class SafetySettings(BaseModel):
class PrebuiltVoiceConfig (line 147) | class PrebuiltVoiceConfig(BaseModel):
class VoiceConfig (line 150) | class VoiceConfig(BaseModel):
class SpeechConfig (line 153) | class SpeechConfig(BaseModel):
class GenerationConfig (line 157) | class GenerationConfig(BaseModel):
method stop_sequences_validator (line 186) | def stop_sequences_validator(cls, value: list[str]) -> list[str]:
class ThinkingConfig (line 191) | class ThinkingConfig(BaseModel):
class ChatRequest (line 195) | class ChatRequest(BaseModel):
FILE: tests/llm_adapters/mock_app/models/openai.py
class ImageUrl (line 5) | class ImageUrl(BaseModel):
class InputAudio (line 9) | class InputAudio(BaseModel):
class File (line 13) | class File(BaseModel):
class TextContent (line 18) | class TextContent(BaseModel):
class ImageContent (line 22) | class ImageContent(BaseModel):
class AudioContent (line 26) | class AudioContent(BaseModel):
class FileContent (line 30) | class FileContent(BaseModel):
class RefusalContent (line 34) | class RefusalContent(BaseModel):
class Function (line 41) | class Function(BaseModel):
class ToolCall (line 45) | class ToolCall(BaseModel):
class DeveloperMessage (line 50) | class DeveloperMessage(BaseModel):
class SystemMessage (line 55) | class SystemMessage(BaseModel):
class UserMessage (line 60) | class UserMessage(BaseModel):
class AssistantMessage (line 65) | class AssistantMessage(BaseModel):
class ToolMessage (line 73) | class ToolMessage(BaseModel):
class TopAudio (line 80) | class TopAudio(BaseModel):
class StaticContent (line 85) | class StaticContent(BaseModel):
class ChatRequest (line 89) | class ChatRequest(BaseModel):
method validate_top_logprobs (line 120) | def validate_top_logprobs(self) -> Self:
class EmbeddingRequest (line 127) | class EmbeddingRequest(BaseModel):
method custom_validate (line 135) | def custom_validate(self) -> Self:
FILE: tests/llm_adapters/mock_app/ollama.py
class EmbeddingRequest (line 6) | class EmbeddingRequest(BaseModel):
function embedding (line 14) | async def embedding(request: EmbeddingRequest = Body(...)) -> dict:
FILE: tests/llm_adapters/mock_app/openai.py
function completions (line 10) | async def completions(request: ChatRequest = Body(...)) -> dict:
function embeddings (line 49) | async def embeddings(request: EmbeddingRequest = Body(...)) -> dict:
FILE: tests/llm_adapters/mock_app/voyage.py
class EmbeddingRequest (line 7) | class EmbeddingRequest(BaseModel):
class ReRankRequest (line 11) | class ReRankRequest(BaseModel):
class TextContent (line 17) | class TextContent(BaseModel):
class ImageBase64Content (line 21) | class ImageBase64Content(BaseModel):
class ImageUrlContent (line 25) | class ImageUrlContent(BaseModel):
class CombinedContent (line 29) | class CombinedContent(BaseModel):
class MultiModalRequest (line 32) | class MultiModalRequest(BaseModel):
function get_embeddings (line 39) | async def get_embeddings(request: EmbeddingRequest = Body(...)) -> dict:
function get_multimodal_embeddings (line 56) | async def get_multimodal_embeddings(request: MultiModalRequest = Body(.....
function get_rerank (line 75) | async def get_rerank(request: ReRankRequest = Body(...)) -> dict:
FILE: tests/llm_adapters/test_gemini_adapter.py
class TestGeminiAdapter (line 12) | class TestGeminiAdapter:
method gemini_adapter (line 14) | def gemini_adapter(self, mock_media_manager, mock_tracer) -> GeminiAda...
method test_chat (line 25) | def test_chat(self, gemini_adapter):
method test_embed (line 46) | def test_embed(self, gemini_adapter: GeminiAdapter):
FILE: tests/llm_adapters/test_ollama_adapter.py
class TestOllamaAdapter (line 8) | class TestOllamaAdapter:
method ollama_adapter (line 10) | def ollama_adapter(self, mock_media_manager) -> OllamaAdapter:
method test_embedding (line 18) | def test_embedding(self, ollama_adapter: OllamaAdapter):
method test_embedding_with_image (line 28) | def test_embedding_with_image(self, ollama_adapter: OllamaAdapter):
FILE: tests/llm_adapters/test_openai_adapter.py
class TestOpenAIAdapter (line 10) | class TestOpenAIAdapter:
method openai_adapter (line 12) | def openai_adapter(self, mock_media_manager, mock_tracer) -> OpenAIAda...
method test_embed (line 25) | def test_embed(self, openai_adapter: OpenAIAdapter):
method test_embed_with_image (line 35) | def test_embed_with_image(self, openai_adapter: OpenAIAdapter):
method test_embed_with_input_out_of_range (line 44) | def test_embed_with_input_out_of_range(self, openai_adapter: OpenAIAda...
method test_old_embedding_model_rasies_error (line 53) | def test_old_embedding_model_rasies_error(self, openai_adapter: OpenAI...
method test_normal_chat (line 64) | def test_normal_chat(self, openai_adapter: OpenAIAdapter):
FILE: tests/llm_adapters/test_voyage_adapter.py
class TestVoyageAdapter (line 11) | class TestVoyageAdapter:
method voyage_adapter (line 13) | def voyage_adapter(self, mock_media_manager):
method test_embedding (line 22) | def test_embedding(self, voyage_adapter: VoyageAdapter):
method test_multi_modal_embedding (line 36) | def test_multi_modal_embedding(self, voyage_adapter: VoyageAdapter):
method test_rerank_without_sort (line 51) | def test_rerank_without_sort(self, voyage_adapter: VoyageAdapter):
method test_rerank_with_sort (line 72) | def test_rerank_with_sort(self, voyage_adapter: VoyageAdapter):
method test_rerank_sort_raise_error (line 88) | def test_rerank_sort_raise_error(self, voyage_adapter: VoyageAdapter):
FILE: tests/memory/test_composer_decomposer.py
function composer (line 14) | def composer():
function decomposer (line 21) | def decomposer():
function multi_decomposer (line 26) | def multi_decomposer():
function group_sender (line 30) | def group_sender():
function c2c_sender (line 37) | def c2c_sender():
class TestDefaultMemoryComposer (line 41) | class TestDefaultMemoryComposer:
method test_compose_group_message (line 42) | def test_compose_group_message(self, composer, group_sender):
method test_compose_c2c_message (line 53) | def test_compose_c2c_message(self, composer, c2c_sender):
method test_compose_llm_response (line 64) | def test_compose_llm_response(self, composer, c2c_sender):
method test_compose_llm_tool_call_message (line 73) | def test_compose_llm_tool_call_message(self, composer, c2c_sender):
method test_compose_llm_tool_result_message (line 81) | def test_compose_llm_tool_result_message(self, composer, c2c_sender):
class TestDefaultMemoryDecomposer (line 88) | class TestDefaultMemoryDecomposer:
method test_decompose_mixed_entries (line 89) | def test_decompose_mixed_entries(self, decomposer, group_sender, c2c_s...
method test_decompose_empty (line 110) | def test_decompose_empty(self, decomposer):
method test_decompose_max_entries (line 114) | def test_decompose_max_entries(self, decomposer, c2c_sender):
class TestMultiElementDecomposer (line 129) | class TestMultiElementDecomposer:
method test_decompose_tool_call_and_result_message (line 130) | def test_decompose_tool_call_and_result_message(self, multi_decomposer...
FILE: tests/memory/test_composer_strategy.py
function mock_container (line 19) | def mock_container():
function sample_context (line 27) | def sample_context():
class TestDropThinkPart (line 35) | class TestDropThinkPart:
method test_drop_think_part_with_think_tag (line 36) | def test_drop_think_part_with_think_tag(self):
method test_drop_think_part_without_think_tag (line 41) | def test_drop_think_part_without_think_tag(self):
class TestTextMessageProcessor (line 46) | class TestTextMessageProcessor:
method test_process (line 47) | def test_process(self, mock_container, sample_context):
class TestMediaMessageProcessor (line 55) | class TestMediaMessageProcessor:
method test_process (line 56) | def test_process(self, mock_container, sample_context):
class TestLLMChatTextContentProcessor (line 65) | class TestLLMChatTextContentProcessor:
method test_process_normal_text (line 66) | def test_process_normal_text(self, mock_container, sample_context):
method test_process_with_think_tag (line 73) | def test_process_with_think_tag(self, mock_container, sample_context):
class TestLLMChatImageContentProcessor (line 81) | class TestLLMChatImageContentProcessor:
method test_process (line 82) | def test_process(self, mock_container, sample_context):
class TestLLMToolCallContentProcessor (line 99) | class TestLLMToolCallContentProcessor:
method test_process (line 100) | def test_process(self, mock_container, sample_context):
class TestLLMToolResultContentProcessor (line 120) | class TestLLMToolResultContentProcessor:
method test_process (line 121) | def test_process(self, mock_container, sample_context):
class TestIMMessageProcessor (line 144) | class TestIMMessageProcessor:
method test_process_with_text_message (line 145) | def test_process_with_text_message(self, mock_container, sample_context):
method test_process_with_media_message (line 159) | def test_process_with_media_message(self, mock_container, sample_conte...
class TestLLMChatMessageProcessor (line 176) | class TestLLMChatMessageProcessor:
method test_process_with_text_content (line 177) | def test_process_with_text_content(self, mock_container, sample_context):
method test_process_with_mixed_content (line 189) | def test_process_with_mixed_content(self, mock_container, sample_conte...
method test_process_with_tool_content (line 213) | def test_process_with_tool_content(self, mock_container, sample_context):
class TestProcessorFactory (line 232) | class TestProcessorFactory:
method test_get_processor_for_im_message (line 233) | def test_get_processor_for_im_message(self, mock_container):
method test_get_processor_for_llm_chat_message (line 239) | def test_get_processor_for_llm_chat_message(self, mock_container):
method test_get_processor_unknown_type (line 245) | def test_get_processor_unknown_type(self, mock_container):
FILE: tests/memory/test_decomposer_strategy.py
function mock_container (line 18) | def mock_container():
function sample_entry (line 26) | def sample_entry():
class TestTextContentStrategy (line 49) | class TestTextContentStrategy:
method test_extract_content (line 50) | def test_extract_content(self, sample_entry):
method test_to_llm_content (line 60) | def test_to_llm_content(self):
method test_to_text (line 73) | def test_to_text(self):
class TestMediaContentStrategy (line 86) | class TestMediaContentStrategy:
method test_extract_content (line 87) | def test_extract_content(self, sample_entry):
method test_to_llm_content (line 95) | def test_to_llm_content(self):
method test_to_text (line 109) | def test_to_text(self):
class TestToolCallContentStrategy (line 123) | class TestToolCallContentStrategy:
method test_extract_content (line 124) | def test_extract_content(self, sample_entry):
method test_no_metadata_returns_empty (line 133) | def test_no_metadata_returns_empty(self):
method test_to_llm_content (line 143) | def test_to_llm_content(self):
method test_to_text (line 162) | def test_to_text(self):
class TestToolResultContentStrategy (line 179) | class TestToolResultContentStrategy:
method test_extract_content (line 180) | def test_extract_content(self, sample_entry):
method test_to_llm_content (line 190) | def test_to_llm_content(self):
method test_to_text (line 211) | def test_to_text(self):
class TestContentParser (line 229) | class TestContentParser:
method test_parse_content (line 230) | def test_parse_content(self, sample_entry):
method test_to_llm_message (line 240) | def test_to_llm_message(self):
method test_to_text (line 254) | def test_to_text(self):
class TestDefaultDecomposerStrategy (line 265) | class TestDefaultDecomposerStrategy:
method test_decompose_empty_entries (line 266) | def test_decompose_empty_entries(self):
method test_decompose_with_entries (line 273) | def test_decompose_with_entries(self, sample_entry):
class TestMultiElementDecomposerStrategy (line 283) | class TestMultiElementDecomposerStrategy:
method test_decompose_empty_entries (line 284) | def test_decompose_empty_entries(self):
method test_process_entry_user_content (line 290) | def test_process_entry_user_content(self):
method test_process_entry_with_ai_response (line 305) | def test_process_entry_with_ai_response(self):
method test_merge_adjacent_messages (line 322) | def test_merge_adjacent_messages(self):
FILE: tests/memory/test_memory_manager.py
class DummyMemoryPersistence (line 18) | class DummyMemoryPersistence(MemoryPersistence):
method __init__ (line 23) | def __init__(self):
method load (line 26) | def load(self, scope_key: str) -> List[MemoryEntry]:
method save (line 30) | def save(self, scope_key: str, entries: List[MemoryEntry]) -> None:
method stop (line 34) | def stop(self):
method flush (line 37) | def flush(self):
function container (line 43) | def container():
function memory_manager (line 53) | def memory_manager(container):
function test_entry (line 61) | def test_entry():
function mock_scope (line 72) | def mock_scope():
class TestMemoryManager (line 81) | class TestMemoryManager:
method test_register_scope (line 82) | def test_register_scope(self, memory_manager):
method test_register_composer (line 89) | def test_register_composer(self, memory_manager):
method test_register_decomposer (line 96) | def test_register_decomposer(self, memory_manager):
method test_store_and_query (line 106) | def test_store_and_query(self, memory_manager, test_entry, mock_scope):
method test_max_entries_limit (line 123) | def test_max_entries_limit(self, memory_manager, mock_scope, container):
method test_shutdown (line 142) | def test_shutdown(self, memory_manager, test_entry):
method test_clear_memory (line 157) | def test_clear_memory(self, memory_manager, mock_scope):
FILE: tests/memory/test_persistence.py
function test_dir (line 29) | def test_dir():
function file_persistence (line 36) | def file_persistence(test_dir):
function chat_senders (line 41) | def chat_senders():
function test_entries (line 48) | def test_entries(chat_senders):
function redis_mock (line 67) | def redis_mock():
function redis_persistence (line 72) | def redis_persistence(redis_mock):
class TestFileMemoryPersistence (line 78) | class TestFileMemoryPersistence:
method test_save_and_load (line 79) | def test_save_and_load(self, file_persistence, test_entries, test_dir):
method test_load_nonexistent (line 100) | def test_load_nonexistent(self, file_persistence):
class TestRedisMemoryPersistence (line 105) | class TestRedisMemoryPersistence:
method test_save (line 106) | def test_save(self, redis_persistence, redis_mock, test_entries):
method test_load_with_data (line 111) | def test_load_with_data(self, redis_persistence, redis_mock, chat_send...
method test_load_no_data (line 148) | def test_load_no_data(self, redis_persistence, redis_mock):
FILE: tests/memory/test_scope.py
function group_sender (line 16) | def group_sender():
function c2c_sender (line 23) | def c2c_sender():
function different_group_sender (line 28) | def different_group_sender():
function different_user_sender (line 35) | def different_user_sender():
class TestMemberScope (line 42) | class TestMemberScope:
method scope (line 44) | def scope(self):
method test_get_scope_key_group (line 47) | def test_get_scope_key_group(self, scope, group_sender):
method test_get_scope_key_c2c (line 51) | def test_get_scope_key_c2c(self, scope, c2c_sender):
method test_is_in_scope_group_same_user (line 55) | def test_is_in_scope_group_same_user(self, scope, group_sender):
method test_is_in_scope_group_different_user (line 61) | def test_is_in_scope_group_different_user(
method test_is_in_scope_group_different_group (line 66) | def test_is_in_scope_group_different_group(
method test_is_in_scope_c2c_same_user (line 71) | def test_is_in_scope_c2c_same_user(self, scope, c2c_sender):
method test_is_in_scope_c2c_different_user (line 75) | def test_is_in_scope_c2c_different_user(self, scope, c2c_sender):
method test_is_in_scope_different_chat_type (line 79) | def test_is_in_scope_different_chat_type(self, scope, group_sender, c2...
class TestGroupScope (line 83) | class TestGroupScope:
method scope (line 85) | def scope(self):
method test_get_scope_key_group (line 88) | def test_get_scope_key_group(self, scope, group_sender):
method test_get_scope_key_c2c (line 92) | def test_get_scope_key_c2c(self, scope, c2c_sender):
method test_is_in_scope_group_same_group (line 96) | def test_is_in_scope_group_same_group(
method test_is_in_scope_group_different_group (line 101) | def test_is_in_scope_group_different_group(
method test_is_in_scope_c2c_same_user (line 106) | def test_is_in_scope_c2c_same_user(self, scope, c2c_sender):
method test_is_in_scope_c2c_different_user (line 110) | def test_is_in_scope_c2c_different_user(self, scope, c2c_sender):
method test_is_in_scope_different_chat_type (line 114) | def test_is_in_scope_different_chat_type(self, scope, group_sender, c2...
class TestGlobalScope (line 118) | class TestGlobalScope:
method scope (line 120) | def scope(self):
method test_get_scope_key (line 123) | def test_get_scope_key(self, scope, group_sender, c2c_sender):
method test_is_in_scope_always_true (line 127) | def test_is_in_scope_always_true(self, scope, group_sender, c2c_sender):
FILE: tests/system_blocks/game/test_dice.py
function container (line 10) | def container():
function create_message (line 16) | def create_message():
function test_dice_roll_basic (line 26) | def test_dice_roll_basic(container, create_message):
function test_dice_roll_invalid (line 41) | def test_dice_roll_invalid(container, create_message):
function test_dice_roll_too_many (line 53) | def test_dice_roll_too_many(container, create_message):
function test_dice_roll_with_modifier (line 65) | def test_dice_roll_with_modifier(container, create_message):
function test_dice_roll_multiple_dice (line 84) | def test_dice_roll_multiple_dice(container, create_message):
FILE: tests/system_blocks/game/test_gacha.py
function container (line 10) | def container():
function create_message (line 16) | def create_message():
function test_gacha_single_pull (line 26) | def test_gacha_single_pull(container, create_message):
function test_gacha_ten_pull (line 39) | def test_gacha_ten_pull(container, create_message):
function test_gacha_custom_rates (line 52) | def test_gacha_custom_rates(container, create_message):
FILE: tests/system_blocks/im/test_messages.py
class MockIMAdapter (line 15) | class MockIMAdapter(IMAdapter):
method send_message (line 16) | async def send_message(self, message, target=None):
method convert_to_message (line 19) | def convert_to_message(self, message):
method start (line 22) | async def start(self):
method stop (line 25) | async def stop(self):
class MockIMManager (line 29) | class MockIMManager(IMManager):
method __init__ (line 30) | def __init__(self):
method get_adapter (line 33) | def get_adapter(self, name):
function container (line 38) | def container():
function test_send_im_message_async (line 53) | async def test_send_im_message_async():
function test_get_im_message (line 99) | def test_get_im_message(container):
function test_im_message_to_text (line 116) | def test_im_message_to_text(container):
function test_text_to_im_message (line 136) | def test_text_to_im_message():
function test_append_im_message (line 165) | def test_append_im_message():
FILE: tests/system_blocks/im/test_states.py
class MockAdapter (line 11) | class MockAdapter(IMAdapter, EditStateAdapter):
method set_chat_editing_state (line 12) | async def set_chat_editing_state(self, *args, **kwargs):
function test_toggle_edit_state_async (line 16) | async def test_toggle_edit_state_async():
FILE: tests/system_blocks/llm/test_basic.py
function container (line 10) | def container():
function test_llm_response_to_text (line 15) | def test_llm_response_to_text():
FILE: tests/system_blocks/llm/test_chat.py
function get_tools (line 19) | def get_tools() -> list[Tool]:
function get_llm_tool_calls (line 46) | def get_llm_tool_calls() -> list[ToolCall]:
class MockLLM (line 58) | class MockLLM:
method chat (line 59) | def chat(self, request):
class MockLLMWithToolCalls (line 73) | class MockLLMWithToolCalls:
method __init__ (line 74) | def __init__(self, with_tool_calls=True):
method chat (line 78) | def chat(self, request):
class MockLLMManager (line 112) | class MockLLMManager(LLMManager):
method __init__ (line 113) | def __init__(self):
method get_llm_id_by_ability (line 116) | def get_llm_id_by_ability(self, ability):
method get_llm (line 119) | def get_llm(self, model_id):
class MockLLMManagerWithToolCalls (line 122) | class MockLLMManagerWithToolCalls(LLMManager):
method __init__ (line 123) | def __init__(self, with_tool_calls=True):
method get_llm_id_by_ability (line 126) | def get_llm_id_by_ability(self, ability):
method get_llm (line 129) | def get_llm(self, model_id):
function container (line 133) | def container():
function test_chat_message_constructor (line 182) | def test_chat_message_constructor(mock_execute):
function test_chat_completion (line 218) | def test_chat_completion(container):
function test_chat_response_converter (line 238) | def test_chat_response_converter():
function test_chat_completion_with_tools (line 273) | def test_chat_completion_with_tools(container):
function test_chat_completion_with_tools_no_tool_calls (line 307) | def test_chat_completion_with_tools_no_tool_calls(container):
FILE: tests/system_blocks/llm/test_image.py
function container (line 13) | def container():
function test_simple_stable_diffusion_webui (line 26) | def test_simple_stable_diffusion_webui(container):
FILE: tests/system_blocks/memory/test_chat_memory.py
class MockMemoryManager (line 16) | class MockMemoryManager(MemoryManager):
method __init__ (line 17) | def __init__(self):
method query (line 21) | def query(self, *args, **kwargs):
method store (line 24) | def store(self, *args, **kwargs):
method clear (line 27) | def clear(self, *args, **kwargs):
class MockScope (line 32) | class MockScope:
method __init__ (line 33) | def __init__(self, name):
class MockScopeRegistry (line 38) | class MockScopeRegistry(ScopeRegistry):
method get_scope (line 39) | def get_scope(self, name):
class MockComposer (line 44) | class MockComposer:
method compose (line 45) | def compose(self, sender, messages):
class MockComposerRegistry (line 50) | class MockComposerRegistry(ComposerRegistry):
method get_composer (line 51) | def get_composer(self, name):
class MockDecomposer (line 56) | class MockDecomposer:
method decompose (line 57) | def decompose(self, memory_entries):
class MockDecomposerRegistry (line 62) | class MockDecomposerRegistry(DecomposerRegistry):
method get_decomposer (line 63) | def get_decomposer(self, name):
function test_chat_memory_query_async (line 68) | async def test_chat_memory_query_async():
function test_chat_memory_store_async (line 95) | async def test_chat_memory_store_async():
FILE: tests/system_blocks/memory/test_clear_memory.py
class MockMemoryManager (line 15) | class MockMemoryManager(MemoryManager):
method __init__ (line 16) | def __init__(self, container: DependencyContainer):
method clear_memory (line 22) | def clear_memory(self, *args, **kwargs):
class MockScope (line 27) | class MockScope:
method __init__ (line 28) | def __init__(self, name):
method get_scope_key (line 31) | def get_scope_key(self, sender: ChatSender):
class MockScopeRegistry (line 36) | class MockScopeRegistry(ScopeRegistry):
method get_scope (line 37) | def get_scope(self, name):
function test_clear_memory_async (line 42) | async def test_clear_memory_async():
FILE: tests/system_blocks/system/test_basic.py
function container (line 13) | def container():
function test_text_block (line 18) | def test_text_block():
function test_text_concat_block (line 31) | def test_text_concat_block():
function test_text_replace_block (line 44) | def test_text_replace_block():
function test_text_extract_by_regex_block (line 61) | def test_text_extract_by_regex_block():
function test_current_time_block (line 78) | def test_current_time_block():
FILE: tests/system_blocks/system/test_help.py
function container (line 12) | def container():
function create_mock_rule (line 20) | def create_mock_rule(
function test_generate_help_basic (line 36) | def test_generate_help_basic(container):
function test_generate_help_empty (line 103) | def test_generate_help_empty(container):
function test_generate_help_no_description (line 124) | def test_generate_help_no_description(container):
function test_generate_help_complex_rules (line 159) | def test_generate_help_complex_rules(container):
FILE: tests/test_config_loader.py
class TestConfig (line 9) | class TestConfig(BaseModel):
class TestConfigLoader (line 16) | class TestConfigLoader(unittest.TestCase):
method setUp (line 17) | def setUp(self):
method test_save_config_with_backup (line 21) | def test_save_config_with_backup(self):
method test_save_config_without_backup (line 54) | def test_save_config_without_backup(self):
FILE: tests/test_game_blocks.py
function container (line 13) | def container():
function create_message (line 18) | def create_message():
function test_dice_roll_basic (line 25) | def test_dice_roll_basic(container, create_message):
function test_dice_roll_invalid (line 39) | def test_dice_roll_invalid(container, create_message):
function test_dice_roll_too_many (line 51) | def test_dice_roll_too_many(container, create_message):
function test_gacha_single_pull (line 63) | def test_gacha_single_pull(container, create_message):
function test_gacha_ten_pull (line 78) | def test_gacha_ten_pull(container, create_message):
function test_gacha_custom_rates (line 92) | def test_gacha_custom_rates(container, create_message):
FILE: tests/test_mcp_server.py
function stdio_config (line 14) | def stdio_config():
function sse_config (line 23) | def sse_config():
function invalid_config (line 31) | def invalid_config():
class MockClientSession (line 38) | class MockClientSession:
method __init__ (line 39) | def __init__(self):
method __aenter__ (line 52) | async def __aenter__(self):
method __aexit__ (line 55) | async def __aexit__(self, exc_type, exc_val, exc_tb):
function test_init (line 59) | def test_init(stdio_config):
function test_connect_disconnect_stdio (line 70) | async def test_connect_disconnect_stdio(stdio_config):
function test_connect_disconnect_sse (line 93) | async def test_connect_disconnect_sse(sse_config):
function test_connect_invalid_config (line 116) | async def test_connect_invalid_config(invalid_config):
function test_connect_timeout (line 124) | async def test_connect_timeout(stdio_config):
function test_tool_methods (line 142) | async def test_tool_methods(stdio_config):
function test_complete (line 166) | async def test_complete(stdio_config):
function test_prompt_methods (line 185) | async def test_prompt_methods(stdio_config):
function test_resource_methods (line 209) | async def test_resource_methods(stdio_config):
FILE: tests/test_media.py
class TestMediaManager (line 12) | class TestMediaManager(unittest.TestCase):
method setUp (line 15) | def setUp(self):
method tearDown (line 75) | def tearDown(self):
method test_register_from_path (line 80) | def test_register_from_path(self):
method test_register_from_data (line 107) | def test_register_from_data(self):
method test_register_from_url (line 133) | def test_register_from_url(self):
method test_format_detection (line 167) | def test_format_detection(self):
method test_reference_management (line 212) | def test_reference_management(self):
method test_search (line 240) | def test_search(self):
method test_media_message (line 281) | def test_media_message(self):
method test_media_message_with_different_formats (line 331) | def test_media_message_with_different_formats(self):
FILE: tests/test_media_element.py
function test_media_element_from_path (line 21) | async def test_media_element_from_path():
function test_media_element_from_url (line 41) | async def test_media_element_from_url():
function test_media_element_from_data (line 63) | async def test_media_element_from_data():
function test_media_element_format_detection (line 86) | async def test_media_element_format_detection():
function test_media_element_errors (line 94) | async def test_media_element_errors():
FILE: tests/test_system_blocks.py
function container (line 12) | def container():
function create_mock_rule (line 19) | def create_mock_rule(
function test_generate_help_basic (line 35) | def test_generate_help_basic(container):
function test_generate_help_empty (line 102) | def test_generate_help_empty(container):
function test_generate_help_no_description (line 123) | def test_generate_help_no_description(container):
function test_generate_help_complex_rules (line 158) | def test_generate_help_complex_rules(container):
FILE: tests/test_workflow_builder.py
class SimpleInputBlock (line 15) | class SimpleInputBlock(Block):
method __init__ (line 22) | def __init__(self, param1: str = "default"):
method execute (line 26) | def execute(self) -> Dict[str, Any]:
class SimpleProcessBlock (line 30) | class SimpleProcessBlock(Block):
method __init__ (line 37) | def __init__(self, multiplier: int = 1):
method execute (line 41) | def execute(self, in1: str) -> Dict[str, Any]:
function setup_module (line 45) | def setup_module(module):
function teardown_module (line 53) | def teardown_module(module):
class TestWorkflowBuilder (line 58) | class TestWorkflowBuilder:
method container (line 60) | def container(self):
method yaml_path (line 70) | def yaml_path(self):
method test_basic_dsl_construction (line 77) | def test_basic_dsl_construction(self, container):
method test_parallel_construction (line 92) | def test_parallel_construction(self, container):
method test_save_and_load (line 112) | def test_save_and_load(self, container, yaml_path):
method test_complex_workflow_serialization (line 148) | def test_complex_workflow_serialization(self, container, yaml_path):
method test_invalid_yaml_handling (line 185) | def test_invalid_yaml_handling(self, container):
method test_unregistered_block_warning (line 190) | def test_unregistered_block_warning(self, container, yaml_path):
method test_registered_block_no_warning (line 200) | def test_registered_block_no_warning(self, container, yaml_path):
FILE: tests/test_workflow_factories.py
function container (line 11) | def container():
function test_game_dice_workflow (line 15) | def test_game_dice_workflow(container):
function test_game_gacha_workflow (line 27) | def test_game_gacha_workflow(container):
function test_system_help_workflow (line 39) | def test_system_help_workflow(container):
FILE: tests/tracing/test_base.py
class TestTraceRecord (line 19) | class TestTraceRecord(TraceRecord):
method update_from_event (line 27) | def update_from_event(self, event):
method to_dict (line 30) | def to_dict(self) -> Dict[str, Any]:
method to_detail_dict (line 33) | def to_detail_dict(self) -> Dict[str, Any]:
class TracingTestBase (line 37) | class TracingTestBase(unittest.TestCase):
method setUp (line 40) | def setUp(self):
method tearDown (line 54) | def tearDown(self):
method create_test_request (line 58) | def create_test_request(self, model: str = "test-model") -> LLMChatReq...
method create_test_response (line 65) | def create_test_response(self, usage: Optional[Usage] = None) -> LLMCh...
method create_test_trace (line 79) | def create_test_trace(self) -> LLMRequestTrace:
FILE: tests/tracing/test_core.py
class TestEvent (line 11) | class TestEvent(TraceEvent):
method __init__ (line 14) | def __init__(self, trace_id: str):
class TestTracer (line 19) | class TestTracer(TracerBase[TestTraceRecord]):
method __init__ (line 25) | def __init__(self, container: DependencyContainer):
method _register_event_handlers (line 28) | def _register_event_handlers(self):
method _unregister_event_handlers (line 31) | def _unregister_event_handlers(self):
method _on_test_event (line 34) | def _on_test_event(self, event: TestEvent):
class TestTracerBase (line 42) | class TestTracerBase(TracingTestBase, IsolatedAsyncioTestCase):
method setUp (line 45) | def setUp(self):
method tearDown (line 50) | def tearDown(self):
method test_generate_trace_id (line 54) | def test_generate_trace_id(self):
method test_get_traces (line 63) | def test_get_traces(self):
method test_get_recent_traces (line 86) | def test_get_recent_traces(self):
method test_get_trace_by_id (line 97) | def test_get_trace_by_id(self):
method test_websocket_operations (line 111) | async def test_websocket_operations(self):
method test_save_and_update_trace_record (line 130) | def test_save_and_update_trace_record(self):
FILE: tests/tracing/test_decorator.py
class TestLLMAdapter (line 11) | class TestLLMAdapter(LLMBackendAdapter, LLMChatProtocol):
method __init__ (line 14) | def __init__(self, tracer: LLMTracer):
method chat (line 19) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
class TestTraceDecorator (line 25) | class TestTraceDecorator(TracingTestBase):
method setUp (line 28) | def setUp(self):
method tearDown (line 34) | def tearDown(self):
method test_trace_success (line 38) | def test_trace_success(self):
method test_trace_failure (line 56) | def test_trace_failure(self):
FILE: tests/tracing/test_llm_tracer.py
class TestLLMTracer (line 8) | class TestLLMTracer(TracingTestBase):
method setUp (line 11) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method test_start_request_tracking (line 20) | def test_start_request_tracking(self):
method test_complete_request_tracking (line 34) | def test_complete_request_tracking(self):
method test_fail_request_tracking (line 50) | def test_fail_request_tracking(self):
method test_event_handlers (line 66) | def test_event_handlers(self):
method test_get_statistics (line 113) | def test_get_statistics(self):
FILE: tests/tracing/test_manager.py
class TestTracingManager (line 9) | class TestTracingManager(TracingTestBase, IsolatedAsyncioTestCase):
method setUp (line 12) | def setUp(self):
method test_register_tracer (line 16) | def test_register_tracer(self):
method test_register_duplicate_tracer (line 25) | def test_register_duplicate_tracer(self):
method test_get_tracer (line 34) | def test_get_tracer(self):
method test_get_all_tracers (line 43) | def test_get_all_tracers(self):
method test_initialize_and_shutdown (line 55) | def test_initialize_and_shutdown(self):
method test_websocket_operations (line 66) | async def test_websocket_operations(self):
method test_trace_operations (line 84) | def test_trace_operations(self):
FILE: tests/tracing/test_models.py
class TestLLMRequestTrace (line 7) | class TestLLMRequestTrace(TracingTestBase):
method setUp (line 10) | def setUp(self):
method test_update_from_start_event (line 14) | def test_update_from_start_event(self):
method test_update_from_complete_event (line 32) | def test_update_from_complete_event(self):
method test_update_from_fail_event (line 54) | def test_update_from_fail_event(self):
method test_to_dict (line 72) | def test_to_dict(self):
method test_request_response_properties (line 100) | def test_request_response_properties(self):
FILE: tests/utils/auth_test_utils.py
function setup_auth_service (line 12) | def setup_auth_service(container: DependencyContainer) -> None:
function auth_headers (line 20) | async def auth_headers(test_client):
FILE: tests/utils/test_block_registry.py
function create_test_block_registry (line 4) | def create_test_block_registry() -> BlockRegistry:
FILE: tests/web/api/im/test_im.py
class DummyConfig (line 32) | class DummyConfig(BaseModel):
class DummyAdapter (line 39) | class DummyAdapter(IMAdapter):
method __init__ (line 44) | def __init__(self, config: DummyConfig):
method convert_to_message (line 50) | def convert_to_message(self, raw_message: Any) -> IMMessage:
method send_message (line 59) | async def send_message(self, message: IMMessage, recipient: ChatSender):
method start (line 63) | async def start(self):
method stop (line 67) | async def stop(self):
function app (line 74) | def app():
function test_client (line 125) | def test_client(app):
class TestIMAdapter (line 131) | class TestIMAdapter:
method test_get_adapter_types (line 133) | async def test_get_adapter_types(self, test_client, auth_headers):
method test_list_adapters (line 144) | async def test_list_adapters(self, test_client, auth_headers):
method test_get_adapter (line 159) | async def test_get_adapter(self, test_client, auth_headers):
method test_create_adapter (line 173) | async def test_create_adapter(self, test_client, auth_headers):
method test_update_adapter (line 198) | async def test_update_adapter(self, test_client, auth_headers):
method test_stop_adapter (line 226) | async def test_stop_adapter(self, test_client, auth_headers):
method test_start_adapter (line 244) | async def test_start_adapter(self, test_client, auth_headers):
method test_delete_adapter (line 263) | async def test_delete_adapter(self, test_client, auth_headers):
method test_get_adapter_config_schema (line 285) | async def test_get_adapter_config_schema(self, test_client, auth_heade...
method test_get_adapter_config_schema_not_found (line 311) | async def test_get_adapter_config_schema_not_found(self, test_client, ...
FILE: tests/web/api/llm/test_llm.py
class TestConfig (line 28) | class TestConfig(BaseModel):
class TestAdapter (line 36) | class TestAdapter(LLMBackendAdapter, LLMChatProtocol):
method __init__ (line 41) | def __init__(self, config: TestConfig):
method chat (line 44) | def chat(self, req: LLMChatRequest) -> LLMChatResponse:
function app (line 62) | def app():
function test_client (line 103) | def test_client(app):
class TestLLMBackend (line 109) | class TestLLMBackend:
method test_get_adapter_types (line 111) | async def test_get_adapter_types(self, test_client, auth_headers):
method test_list_backends (line 122) | async def test_list_backends(self, test_client, auth_headers):
method test_get_backend (line 137) | async def test_get_backend(self, test_client, auth_headers):
method test_create_backend (line 150) | async def test_create_backend(self, test_client, auth_headers):
method test_update_backend (line 180) | async def test_update_backend(self, test_client, auth_headers):
method test_delete_backend (line 209) | async def test_delete_backend(self, test_client, auth_headers):
method test_get_adapter_config_schema (line 231) | async def test_get_adapter_config_schema(self, test_client, auth_heade...
method test_get_adapter_config_schema_not_found (line 256) | async def test_get_adapter_config_schema_not_found(self, test_client, ...
FILE: tests/web/api/media/test_media.py
function temp_media_dir (line 27) | def temp_media_dir():
function container (line 40) | def container(temp_media_dir):
function app (line 72) | def app(container):
function test_client (line 80) | def test_client(app):
class TestMediaAPI (line 89) | class TestMediaAPI:
method setup_mocks (line 91) | def setup_mocks(self, container, temp_media_dir):
method test_get_system_info (line 130) | def test_get_system_info(self, test_client, auth_headers, container):
method test_set_config (line 184) | def test_set_config(self, test_client, auth_headers, container):
method test_cleanup_unreferenced (line 222) | def test_cleanup_unreferenced(self, test_client, auth_headers, contain...
FILE: tests/web/api/plugin/test_plugin.py
function make_test_plugin (line 30) | def make_test_plugin():
function MOCK_PLUGIN_SEARCH_RESPONSE (line 53) | async def MOCK_PLUGIN_SEARCH_RESPONSE():
function MOCK_PLUGIN_INFO_RESPONSE (line 81) | async def MOCK_PLUGIN_INFO_RESPONSE():
function app (line 102) | def app():
function test_client (line 139) | def test_client(app):
class TestPlugin (line 145) | class TestPlugin:
method test_search_plugins (line 147) | async def test_search_plugins(self, test_client, auth_headers):
method test_get_plugin_info (line 165) | async def test_get_plugin_info(self, test_client, auth_headers):
method test_get_plugin_details (line 184) | async def test_get_plugin_details(self, test_client, auth_headers):
method test_get_nonexistent_plugin (line 199) | async def test_get_nonexistent_plugin(self, test_client, auth_headers):
method test_update_plugin (line 210) | async def test_update_plugin(self, test_client, auth_headers):
method test_enable_plugin (line 222) | async def test_enable_plugin(self, test_client, auth_headers):
method test_disable_plugin (line 239) | async def test_disable_plugin(self, test_client, auth_headers):
method test_install_plugin (line 256) | async def test_install_plugin(self, test_client, auth_headers):
method test_uninstall_plugin (line 289) | async def test_uninstall_plugin(self, test_client, auth_headers):
FILE: tests/web/api/system/test_system.py
function app (line 22) | def app():
function test_client (line 62) | def test_client(app):
class TestSystemStatus (line 68) | class TestSystemStatus:
method test_get_system_status (line 70) | async def test_get_system_status(self, test_client, auth_headers):
method test_get_system_status_unauthorized (line 120) | async def test_get_system_status_unauthorized(self, test_client):
method test_check_update (line 130) | async def test_check_update(self, test_client, auth_headers):
FILE: tests/web/api/workflow/test_workflow.py
class MessageBlock (line 25) | class MessageBlock(Block):
method __init__ (line 31) | def __init__(self, text: str = ""):
method execute (line 35) | def execute(self) -> dict:
class LLMBlock (line 39) | class LLMBlock(Block):
method __init__ (line 45) | def __init__(self, prompt: str = ""):
method execute (line 50) | def execute(self, input: str) -> dict:
function app (line 56) | def app():
function test_client (line 94) | def test_client(app):
class TestWorkflow (line 100) | class TestWorkflow:
method test_list_workflows (line 102) | async def test_list_workflows(self, test_client, auth_headers):
method test_get_workflow (line 118) | async def test_get_workflow(self, test_client, auth_headers):
method test_create_workflow (line 134) | async def test_create_workflow(self, test_client, auth_headers):
method test_update_workflow (line 167) | async def test_update_workflow(self, test_client, auth_headers):
method test_delete_workflow (line 202) | async def test_delete_workflow(self, test_client, auth_headers):
FILE: tests/web/auth/test_auth.py
function app (line 18) | def app():
function test_client (line 29) | def test_client(app):
function auth_token (line 35) | async def auth_token(test_client):
class TestAuth (line 46) | class TestAuth:
method test_check_first_time (line 48) | async def test_check_first_time(self, test_client):
method test_normal_login (line 70) | async def test_normal_login(self, test_client):
method test_login_wrong_password (line 88) | async def test_login_wrong_password(self, test_client):
method test_change_password (line 105) | async def test_change_password(self, test_client, auth_token):
method test_change_password_wrong_old (line 122) | async def test_change_password_wrong_old(self, test_client, auth_token):
FILE: tests/workflow_executor/test_block.py
function test_block_creation (line 18) | def test_block_creation():
function test_block_execute (line 25) | def test_block_execute():
FILE: tests/workflow_executor/test_executor.py
class InputBlock (line 16) | class InputBlock(Block):
method execute (line 24) | def execute(self, **kwargs):
class ProcessBlock (line 28) | class ProcessBlock(Block):
method execute (line 41) | def execute(self, input1: str, **kwargs):
class OutputBlock (line 45) | class OutputBlock(Block):
method execute (line 53) | def execute(self, input1: str, **kwargs):
class FailingBlock (line 57) | class FailingBlock(Block):
method execute (line 65) | def execute(self, input1: str, **kwargs):
function test_executor_run (line 116) | async def test_executor_run():
function test_executor_with_failing_block (line 132) | async def test_executor_with_failing_block():
function test_executor_with_no_blocks (line 145) | async def test_executor_with_no_blocks():
function test_executor_with_multiple_outputs (line 159) | async def test_executor_with_multiple_outputs():
FILE: tests/workflow_executor/test_input_output.py
function test_input_validation (line 4) | def test_input_validation():
function test_output_validation (line 23) | def test_output_validation():
FILE: tests/workflow_executor/test_workflow_basic.py
function test_workflow_creation (line 47) | def test_workflow_creation():
Condensed preview — 338 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,242K chars).
[
{
"path": ".cursor/rules/create-workflow.mdc",
"chars": 3362,
"preview": "---\ndescription: 创建 Workflow\nglobs: \n---\nYou are an expert in Python, writing an AI application called chatgpt-mirai-qq-"
},
{
"path": ".dockerignore",
"chars": 58,
"preview": "config.json\nconfig.json.old\nconfig.cfg\n.chatgpt_cache.json"
},
{
"path": ".editorconfig",
"chars": 244,
"preview": "# https://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\ninsert"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 456,
"preview": "---\nname: Bug report\nabout: BUG 汇报\ntitle: \"[BUG] 请填写标题\"\nlabels: bug\nassignees: ''\n\n---\n\n**提交 issue 前,请先确认:**\n- [x] 我已看过 "
},
{
"path": ".github/ISSUE_TEMPLATE/feature-request.md",
"chars": 110,
"preview": "---\nname: Feature request\nabout: 提交新功能建议\ntitle: \"[Feature] 请在此处填写标题\"\nlabels: enhancement\nassignees: ''\n\n---\n\n\n"
},
{
"path": ".github/dependabot.yml",
"chars": 502,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/quickstarts/windows/scripts/启动.cmd",
"chars": 418,
"preview": "@REM ...\n\n@ECHO OFF\n\n@CHCP 65001\n\nTITLE [Kirara AI] AI 系统正在启动...\n\nSET PATH=%cd%\\WPy64-31320\\python;%cd%\\ffmpeg\\bin;%PATH"
},
{
"path": ".github/workflows/docker-latest.yml",
"chars": 966,
"preview": "name: Docker build latest\n\non:\n workflow_dispatch:\n\njobs:\n docker:\n runs-on: ubuntu-latest\n steps:\n -\n "
},
{
"path": ".github/workflows/docker-tag.yml",
"chars": 1367,
"preview": "name: Docker build with tags\r\n\r\non:\r\n workflow_dispatch:\r\n push:\r\n tags:\r\n - '**'\r\n\r\njobs:\r\n docker:\r\n run"
},
{
"path": ".github/workflows/pr_review.yml",
"chars": 14096,
"preview": "name: PR Code Review\n\non:\n pull_request_target:\n branches: [ \"master\" ]\n\njobs:\n mypy-review:\n name: MyPy Type Ch"
},
{
"path": ".github/workflows/project_check.yml",
"chars": 6415,
"preview": "name: Project Check\n\non:\n push:\n branches: [ \"master\" ]\n merge_group:\n branches: [ \"master\" ]\n\njobs:\n analyze:\n"
},
{
"path": ".github/workflows/quickstart-windows.yml",
"chars": 4214,
"preview": "name: Windows Quickstart\n\non:\n workflow_dispatch:\n push:\n branches: ['master']\n tags: ['**']\n pull_request:\n "
},
{
"path": ".github/workflows/run-tests.yml",
"chars": 1663,
"preview": "name: Run Tests\n\non:\n workflow_dispatch:\n push:\n branches:\n - '**'\n pull_request:\n branches:\n - maste"
},
{
"path": ".github/workflows/stale.yml",
"chars": 1814,
"preview": "name: 处理不活跃的 Issue 和 PR\n\non:\n workflow_dispatch:\n schedule:\n - cron: '0 0 * * *' # 每天午夜运行\n\npermissions:\n contents"
},
{
"path": ".gitignore",
"chars": 367,
"preview": "config.json\nconfig.cfg\n__pycache__/\npython3.11/\n.idea/\ndata/*.json\n.chatgpt_cache.json\nDockerfile.dev\n**/.DS_Store\nvenv/"
},
{
"path": ".pre-commit-config.yaml",
"chars": 411,
"preview": "repos:\n - repo: https://github.com/PyCQA/isort\n rev: 6.0.0\n hooks:\n - id: isort\n name: isort (python3"
},
{
"path": ".pylintrc",
"chars": 1445,
"preview": "[MASTER]\n# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the\n# number of processors available"
},
{
"path": "Dockerfile",
"chars": 1506,
"preview": "# 第一阶段:构建wheel包\nFROM python:3.11-slim AS builder\n\nWORKDIR /build\nCOPY . .\nRUN python -m pip install build && \\\n pytho"
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "MANIFEST.in",
"chars": 294,
"preview": "recursive-include kirara_ai/plugins/im_http_legacy_adapter/assets *\nrecursive-include kirara_ai/plugins/im_qqbot_adapter"
},
{
"path": "README.md",
"chars": 8915,
"preview": "\n<p align=\"center\">\n <h2 align=\"center\">Kirara AI</h2>\n <p align=\"center\">\n 一款支持主流大语言模型、主流聊天平台的聊天的机器人!\n <br/>\n "
},
{
"path": "alembic.ini",
"chars": 3743,
"preview": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\n# Use forward slashes (/) also on win"
},
{
"path": "config.yaml.example",
"chars": 1848,
"preview": "# 配置文件示例\n# 通讯平台配置部分\nims:\n # 每个 IM 平台的具体配置\n - name: \"telegram-bot-1234\" # IM 平台实例名称\n enable: true # 是否"
},
{
"path": "data/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "data/dispatch_rules/rules.yaml",
"chars": 2028,
"preview": "- rule_id: chat_normal\n name: 群聊AI对话\n description: 群聊中使用 /chat 开头对话或者 被@ 时触发聊天\n workflow_id: chat:normal\n priority: "
},
{
"path": "data/media/.gitignore",
"chars": 18,
"preview": "metadata/*\nfiles/*"
},
{
"path": "data/memory/.gitignore",
"chars": 6,
"preview": "*.json"
},
{
"path": "data/web/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "data/workflows/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "data/workflows/chat/dsr_thinking.yaml",
"chars": 4862,
"preview": "name: 聊天 - 深度思考\ndescription: DeepSeek 思考模型聊天,隐藏 <think> 标签内容\nblocks:\n - type: internal:get_message\n name: get_messag"
},
{
"path": "data/workflows/chat/memory_store.yaml",
"chars": 486,
"preview": "name: 记录聊天内容\ndescription: 默默记下大家的聊天内容,可以使用查询记忆模块读取出来。\nblocks:\n - type: internal:get_message\n name: 6233e64c-433e-408"
},
{
"path": "data/workflows/chat/normal_multimodal.yaml",
"chars": 4226,
"preview": "name: 聊天 - 原生多模态对话\ndescription: 基于原生多模态能力的图文对话,适用于本身支持图片输入/回答的模型,在读取记忆时会恢复原来的媒体资源\nblocks:\n - type: internal:get_message"
},
{
"path": "data/workflows/chat/talk_break.yaml",
"chars": 4077,
"preview": "name: 聊天 - 自定义分段\ndescription: 使用 `<break>` 作为关键词,让 AI 分段回复的工作流\nblocks:\n - type: internal:text_block\n name: system_pr"
},
{
"path": "docker/start.sh",
"chars": 809,
"preview": "#!/bin/bash\ncd /app\n\n# Copy default data\n# check if data directory exists\nif [ ! -d \"/app/data\" ]; then\n echo \"Data d"
},
{
"path": "kirara_ai/__init__.py",
"chars": 212,
"preview": "from .config.config_loader import ConfigLoader\nfrom .entry import init_application, run_application\nfrom .logger import "
},
{
"path": "kirara_ai/__main__.py",
"chars": 1056,
"preview": "import argparse\nimport os\nimport subprocess\nimport sys\n\nfrom kirara_ai.entry import init_application, run_application\nfr"
},
{
"path": "kirara_ai/alembic/README",
"chars": 38,
"preview": "Generic single-database configuration."
},
{
"path": "kirara_ai/alembic/env.py",
"chars": 2201,
"preview": "from logging.config import fileConfig\n\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\n\n# th"
},
{
"path": "kirara_ai/alembic/script.py.mako",
"chars": 689,
"preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom typ"
},
{
"path": "kirara_ai/alembic/versions/4a364dbb8dab_initial_migration.py",
"chars": 3116,
"preview": "\"\"\"Initial migration\n\nRevision ID: 4a364dbb8dab\nRevises: \nCreate Date: 2025-03-29 13:59:33.243069\n\n\"\"\"\nfrom typing impor"
},
{
"path": "kirara_ai/config/__init__.py",
"chars": 383,
"preview": "import os\n\n# 读取DATA_PATH环境变量,若未能找到则以当前工作目录为根文件夹存储在$PWD/data目录下。\nDATA_PATH = os.path.abspath(\n os.environ.get(\"DATA_PA"
},
{
"path": "kirara_ai/config/config_loader.py",
"chars": 2709,
"preview": "import os\nimport shutil\nfrom functools import wraps\nfrom typing import Optional, Type, TypeVar\n\nfrom pydantic import Bas"
},
{
"path": "kirara_ai/config/global_config.py",
"chars": 5915,
"preview": "from typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, ConfigDict, Field, model_validator\n\nfrom k"
},
{
"path": "kirara_ai/database/__init__.py",
"chars": 122,
"preview": "from kirara_ai.database.manager import Base, DatabaseManager, metadata\n\n__all__ = [\"Base\", \"DatabaseManager\", \"metadata\""
},
{
"path": "kirara_ai/database/manager.py",
"chars": 3332,
"preview": "import os\nfrom typing import Optional\n\nfrom alembic import command\nfrom alembic.config import Config\nfrom alembic.runtim"
},
{
"path": "kirara_ai/entry.py",
"chars": 10760,
"preview": "import asyncio\nimport os\nimport signal\nimport time\n\nfrom packaging import version\n\nfrom kirara_ai.config.config_loader i"
},
{
"path": "kirara_ai/events/__init__.py",
"chars": 675,
"preview": "from .application import ApplicationStarted, ApplicationStopping\nfrom .event_bus import EventBus\nfrom .im import IMAdapt"
},
{
"path": "kirara_ai/events/application.py",
"chars": 196,
"preview": "\nclass ApplicationStarted:\n def __repr__(self):\n return f\"{self.__class__.__name__}()\"\n\nclass ApplicationStopp"
},
{
"path": "kirara_ai/events/event_bus.py",
"chars": 987,
"preview": "from typing import Callable, Dict, List, Type\n\nfrom kirara_ai.logger import get_logger\n\nlogger = get_logger(\"EventBus\")\n"
},
{
"path": "kirara_ai/events/im.py",
"chars": 355,
"preview": "from typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n from kirara_ai.im.adapter import IMAdapter\n\n\nclass IMEvent:\n "
},
{
"path": "kirara_ai/events/listen.py",
"chars": 723,
"preview": "import inspect\nfrom typing import Callable\n\nfrom kirara_ai.events.event_bus import EventBus\n\n\ndef listen(event_bus: Even"
},
{
"path": "kirara_ai/events/llm.py",
"chars": 461,
"preview": "from kirara_ai.llm.adapter import LLMBackendAdapter\n\n\nclass LLMAdapterEvent:\n def __init__(self, adapter: LLMBackendA"
},
{
"path": "kirara_ai/events/plugin.py",
"chars": 371,
"preview": "\nfrom kirara_ai.plugin_manager.plugin import Plugin\n\n\nclass PluginEvent:\n def __init__(self, plugin: Plugin):\n "
},
{
"path": "kirara_ai/events/tracing/__init__.py",
"chars": 356,
"preview": "from .base import TraceCompleteEvent, TraceEvent, TraceFailEvent, TraceStartEvent\nfrom .llm import LLMRequestCompleteEve"
},
{
"path": "kirara_ai/events/tracing/base.py",
"chars": 468,
"preview": "\nimport abc\nfrom datetime import datetime\n\n\nclass TraceEvent(abc.ABC):\n \"\"\"跟踪事件基类\"\"\"\n \n def __init__(self, trac"
},
{
"path": "kirara_ai/events/tracing/llm.py",
"chars": 2214,
"preview": "import time\nfrom typing import Union\n\nfrom kirara_ai.llm.format.request import LLMChatRequest\nfrom kirara_ai.llm.format."
},
{
"path": "kirara_ai/events/workflow.py",
"chars": 682,
"preview": "from typing import Any, Dict\n\nfrom kirara_ai.workflow.core.execution.executor import WorkflowExecutor\nfrom kirara_ai.wor"
},
{
"path": "kirara_ai/im/__init__.py",
"chars": 107,
"preview": "from .im_registry import IMRegistry\nfrom .manager import IMManager\n\n__all__ = [\"IMRegistry\", \"IMManager\"]\n\n"
},
{
"path": "kirara_ai/im/adapter.py",
"chars": 1987,
"preview": "from abc import ABC, abstractmethod\nfrom typing import Any, Optional, Protocol\n\nfrom pydantic import BaseModel\nfrom typi"
},
{
"path": "kirara_ai/im/im_registry.py",
"chars": 2459,
"preview": "from typing import Dict, Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom kirara_ai.im.adapter import IMAdapt"
},
{
"path": "kirara_ai/im/manager.py",
"chars": 6575,
"preview": "import asyncio\nfrom typing import Dict, Type\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.config.config_loader import"
},
{
"path": "kirara_ai/im/message.py",
"chars": 11523,
"preview": "from abc import ABC, abstractmethod\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Literal, Optional\n\nfrom"
},
{
"path": "kirara_ai/im/profile.py",
"chars": 944,
"preview": "from enum import Enum, auto\nfrom typing import Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass Gender(Enum):\n "
},
{
"path": "kirara_ai/im/sender.py",
"chars": 2420,
"preview": "from dataclasses import dataclass, field\nfrom enum import Enum\nfrom typing import Any, Dict, Optional\n\n\nclass ChatType(E"
},
{
"path": "kirara_ai/internal.py",
"chars": 282,
"preview": "# 定义优雅退出异常\nimport asyncio\n\nshutdown_event = asyncio.Event()\n\nrestart_flag = False\n\n\ndef set_restart_flag():\n global r"
},
{
"path": "kirara_ai/ioc/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "kirara_ai/ioc/container.py",
"chars": 4380,
"preview": "import contextvars\nfrom typing import Any, Optional, Type, TypeVar, overload\n\nT = TypeVar(\"T\")\n\nclass DependencyContaine"
},
{
"path": "kirara_ai/ioc/inject.py",
"chars": 4105,
"preview": "from functools import wraps\nfrom inspect import signature\nfrom typing import Any, Callable, Optional, Type\n\nfrom kirara_"
},
{
"path": "kirara_ai/llm/adapter.py",
"chars": 1113,
"preview": "from abc import ABC\nfrom typing import List, Protocol, runtime_checkable\n\nfrom kirara_ai.config.global_config import Mod"
},
{
"path": "kirara_ai/llm/format/__init__.py",
"chars": 372,
"preview": "from .message import LLMChatImageContent, LLMChatMessage, LLMChatTextContent, LLMToolCallContent, LLMToolResultContent\nf"
},
{
"path": "kirara_ai/llm/format/embedding.py",
"chars": 1585,
"preview": "from typing import Literal, Optional\nfrom pydantic import BaseModel\n\nfrom .message import LLMChatTextContent, LLMChatIma"
},
{
"path": "kirara_ai/llm/format/message.py",
"chars": 2125,
"preview": "import json\nfrom typing import Literal, Optional, Union\n\nfrom pydantic import BaseModel, field_validator, model_validato"
},
{
"path": "kirara_ai/llm/format/request.py",
"chars": 1124,
"preview": "from typing import Any, List, Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.llm.format.message import LLMChat"
},
{
"path": "kirara_ai/llm/format/rerank.py",
"chars": 2116,
"preview": "from typing import Optional\nfrom typing_extensions import Self\nfrom pydantic import BaseModel, model_validator\n\nfrom .re"
},
{
"path": "kirara_ai/llm/format/response.py",
"chars": 600,
"preview": "from typing import List, Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.llm.format.message import LLMChatMessa"
},
{
"path": "kirara_ai/llm/format/tool.py",
"chars": 3092,
"preview": "import json\nfrom typing import Any, Callable, Coroutine, Generic, List, Literal, Optional, TypeVar, Union\n\nfrom pydantic"
},
{
"path": "kirara_ai/llm/llm_manager.py",
"chars": 8005,
"preview": "import random\nfrom typing import Dict, List, Optional\n\nfrom typing_extensions import deprecated\n\nfrom kirara_ai.config.g"
},
{
"path": "kirara_ai/llm/llm_registry.py",
"chars": 2046,
"preview": "from typing import Dict, Optional, Type\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.logger import get_logger\n\nfrom ."
},
{
"path": "kirara_ai/llm/model_types.py",
"chars": 2611,
"preview": "from abc import abstractmethod\nfrom enum import Enum\n\n\nclass ModelType(Enum):\n \"\"\"\n 模型类型枚举\n \"\"\"\n LLM = \"llm\""
},
{
"path": "kirara_ai/logger.py",
"chars": 6653,
"preview": "import asyncio\nimport json\nimport os\nimport re\nimport traceback\nfrom collections import deque\nfrom datetime import datet"
},
{
"path": "kirara_ai/mcp_module/__init__.py",
"chars": 174,
"preview": "from .manager import MCPServerManager\nfrom .models import MCPConnectionState\nfrom .server import MCPServer\n\n__all__ = [\""
},
{
"path": "kirara_ai/mcp_module/manager.py",
"chars": 15640,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport asyncio\nfrom functools import partial\nfrom typing import Dict, Nam"
},
{
"path": "kirara_ai/mcp_module/models.py",
"chars": 204,
"preview": "from enum import Enum\n\n\nclass MCPConnectionState(Enum):\n DISCONNECTED = \"disconnected\"\n CONNECTING = \"connecting\"\n"
},
{
"path": "kirara_ai/mcp_module/server.py",
"chars": 10885,
"preview": "import asyncio\nfrom contextlib import AsyncExitStack\nfrom typing import Optional\n\nimport anyio\nimport anyio.lowlevel\nfro"
},
{
"path": "kirara_ai/media/__init__.py",
"chars": 352,
"preview": "from kirara_ai.media.manager import MediaManager\nfrom kirara_ai.media.media_object import Media\nfrom kirara_ai.media.met"
},
{
"path": "kirara_ai/media/carrier/__init__.py",
"chars": 225,
"preview": "from .provider import MediaReferenceProvider\nfrom .registry import MediaCarrierRegistry\nfrom .service import MediaCarrie"
},
{
"path": "kirara_ai/media/carrier/provider.py",
"chars": 303,
"preview": "from abc import ABC, abstractmethod\nfrom typing import Generic, Optional, TypeVar\n\nT = TypeVar(\"T\")\n\nclass MediaReferenc"
},
{
"path": "kirara_ai/media/carrier/registry.py",
"chars": 1108,
"preview": "from typing import Dict\n\nfrom kirara_ai.ioc.container import DependencyContainer\n\nfrom .provider import MediaReferencePr"
},
{
"path": "kirara_ai/media/carrier/service.py",
"chars": 4690,
"preview": "from typing import Any, Dict, List, Optional, Tuple\n\nfrom kirara_ai.ioc.container import DependencyContainer\nfrom kirara"
},
{
"path": "kirara_ai/media/manager.py",
"chars": 22727,
"preview": "import asyncio\nimport base64\nimport hashlib\nimport json\nimport shutil\nimport time\nfrom pathlib import Path\nfrom typing i"
},
{
"path": "kirara_ai/media/media_object.py",
"chars": 3868,
"preview": "import base64\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, List, Optional\n\nif TYPE_CHECKING:\n from kira"
},
{
"path": "kirara_ai/media/metadata.py",
"chars": 2518,
"preview": "from datetime import datetime\nfrom typing import Any, Dict, List, Optional, Set\n\nfrom kirara_ai.media.types.media_type i"
},
{
"path": "kirara_ai/media/types/__init__.py",
"chars": 80,
"preview": "from kirara_ai.media.types.media_type import MediaType\n\n__all__ = [\"MediaType\"] "
},
{
"path": "kirara_ai/media/types/media_type.py",
"chars": 523,
"preview": "from enum import Enum\n\n\nclass MediaType(Enum):\n \"\"\"媒体类型枚举\"\"\"\n IMAGE = \"image\"\n AUDIO = \"audio\"\n VIDEO = \"vid"
},
{
"path": "kirara_ai/media/utils/__init__.py",
"chars": 122,
"preview": "from kirara_ai.media.utils.mime import detect_mime_type, mime_remapping\n\n__all__ = [\"detect_mime_type\", \"mime_remapping\""
},
{
"path": "kirara_ai/media/utils/mime.py",
"chars": 1143,
"preview": "from typing import Optional, Tuple\n\nimport magic\n\nfrom kirara_ai.media.types.media_type import MediaType\n\n# MIME类型重映射\nmi"
},
{
"path": "kirara_ai/memory/composes/__init__.py",
"chars": 719,
"preview": "from .base import ComposableMessageType, MemoryComposer, MemoryDecomposer\nfrom .builtin_composes import DefaultMemoryCom"
},
{
"path": "kirara_ai/memory/composes/base.py",
"chars": 1042,
"preview": "from abc import ABC, abstractmethod\nfrom typing import List, Optional, Union\n\nfrom kirara_ai.im.message import IMMessage"
},
{
"path": "kirara_ai/memory/composes/builtin_composes.py",
"chars": 3098,
"preview": "from datetime import datetime\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom kirara_ai.im.message import IMMe"
},
{
"path": "kirara_ai/memory/composes/composer_strategy.py",
"chars": 6702,
"preview": "from abc import ABC, abstractmethod\nfrom typing import Any, Dict, Optional, Type\n\nimport kirara_ai.llm.format.tool as to"
},
{
"path": "kirara_ai/memory/composes/decomposer_strategy.py",
"chars": 14500,
"preview": "from datetime import datetime, timedelta\nfrom typing import Any, Dict, List, NamedTuple, Protocol, cast\n\nfrom kirara_ai."
},
{
"path": "kirara_ai/memory/composes/xml_helper.py",
"chars": 1942,
"preview": "import re\nfrom typing import Dict, List, Optional, Tuple\n\n\nclass XMLHelper:\n \"\"\"XML 格式化和解析的辅助工具类\"\"\"\n\n @staticmetho"
},
{
"path": "kirara_ai/memory/entry.py",
"chars": 355,
"preview": "from dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import Any, Dict\n\nfrom kirara_ai.im.s"
},
{
"path": "kirara_ai/memory/memory_manager.py",
"chars": 6900,
"preview": "from typing import Dict, List, Optional, Type\n\nfrom kirara_ai.config.global_config import GlobalConfig\nfrom kirara_ai.im"
},
{
"path": "kirara_ai/memory/persistences/__init__.py",
"chars": 309,
"preview": "from .base import AsyncMemoryPersistence, MemoryPersistence\nfrom .file_persistence import FileMemoryPersistence\nfrom .re"
},
{
"path": "kirara_ai/memory/persistences/base.py",
"chars": 1711,
"preview": "import threading\nfrom abc import ABC, abstractmethod\nfrom queue import Empty, Queue\nfrom typing import List, Tuple\n\nfrom"
},
{
"path": "kirara_ai/memory/persistences/codecs.py",
"chars": 1690,
"preview": "import json\nfrom datetime import datetime\nfrom types import FunctionType\n\nfrom kirara_ai.im.sender import ChatSender, Ch"
},
{
"path": "kirara_ai/memory/persistences/file_persistence.py",
"chars": 2199,
"preview": "import json\nimport os\nfrom datetime import datetime\nfrom typing import List\n\nfrom kirara_ai.memory.entry import MemoryEn"
},
{
"path": "kirara_ai/memory/persistences/redis_persistence.py",
"chars": 1933,
"preview": "import json\nfrom datetime import datetime\nfrom typing import List, Optional\n\nfrom kirara_ai.memory.entry import MemoryEn"
},
{
"path": "kirara_ai/memory/registry.py",
"chars": 1640,
"preview": "from typing import Dict, Type\n\nfrom kirara_ai.ioc.container import DependencyContainer\nfrom kirara_ai.ioc.inject import "
},
{
"path": "kirara_ai/memory/scopes/__init__.py",
"chars": 166,
"preview": "from .base import MemoryScope\nfrom .builtin_scopes import GlobalScope, GroupScope, MemberScope\n\n__all__ = [\"MemoryScope\""
},
{
"path": "kirara_ai/memory/scopes/base.py",
"chars": 358,
"preview": "from abc import ABC, abstractmethod\n\nfrom kirara_ai.im.sender import ChatSender\n\n\nclass MemoryScope(ABC):\n \"\"\"记忆作用域抽象"
},
{
"path": "kirara_ai/memory/scopes/builtin_scopes.py",
"chars": 1714,
"preview": "from kirara_ai.im.sender import ChatSender, ChatType\n\nfrom .base import MemoryScope\n\n\n# 默认实现\nclass MemberScope(MemorySco"
},
{
"path": "kirara_ai/plugin_manager/models.py",
"chars": 388,
"preview": "from typing import Any, Dict, Optional\n\nfrom pydantic import BaseModel\n\n\nclass PluginInfo(BaseModel):\n \"\"\"插件信息\"\"\"\n\n "
},
{
"path": "kirara_ai/plugin_manager/plugin.py",
"chars": 978,
"preview": "from abc import ABC, abstractmethod\n\nfrom kirara_ai.events.event_bus import EventBus\nfrom kirara_ai.im.im_registry impor"
},
{
"path": "kirara_ai/plugin_manager/plugin_event_bus.py",
"chars": 995,
"preview": "from typing import Callable, List, Type\n\nfrom kirara_ai.events.event_bus import EventBus\n\n\nclass PluginEventBus:\n def"
},
{
"path": "kirara_ai/plugin_manager/plugin_loader.py",
"chars": 19458,
"preview": "import asyncio\nimport importlib\nimport os\nimport sys\nfrom typing import Dict, List, Optional, Type\n\nfrom kirara_ai.confi"
},
{
"path": "kirara_ai/plugin_manager/utils.py",
"chars": 677,
"preview": "from importlib.metadata import PackageNotFoundError, distribution\nfrom typing import Any, Dict, Optional\n\n\ndef get_packa"
},
{
"path": "kirara_ai/plugins/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "kirara_ai/plugins/bundled_frpc/__init__.py",
"chars": 1339,
"preview": "from quart import g\n\nfrom kirara_ai.config.global_config import GlobalConfig\nfrom kirara_ai.logger import get_logger\nfro"
},
{
"path": "kirara_ai/plugins/bundled_frpc/frpc_manager.py",
"chars": 13367,
"preview": "import io\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport tarfile\nimport tempfile\nimport threading\nimpo"
},
{
"path": "kirara_ai/plugins/bundled_frpc/models.py",
"chars": 752,
"preview": "from typing import Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.config.global_config import FrpcConfig\n\n\ncla"
},
{
"path": "kirara_ai/plugins/bundled_frpc/routes.py",
"chars": 6451,
"preview": "import asyncio\n\nfrom quart import Blueprint, Response, g, request\n\nfrom kirara_ai.config.config_loader import CONFIG_FIL"
},
{
"path": "kirara_ai/plugins/im_http_legacy_adapter/__init__.py",
"chars": 1021,
"preview": "import os\n\nfrom im_http_legacy_adapter.adapter import HttpLegacyAdapter, HttpLegacyConfig\n\nfrom kirara_ai.logger import "
},
{
"path": "kirara_ai/plugins/im_http_legacy_adapter/adapter.py",
"chars": 10865,
"preview": "import asyncio\nimport re\nimport time\nfrom typing import Any, Dict, List, Optional, Protocol\n\nfrom fastapi import Body, F"
},
{
"path": "kirara_ai/plugins/im_http_legacy_adapter/setup.py",
"chars": 429,
"preview": "from setuptools import find_packages, setup\n\nsetup(\n name=\"kirara_ai-http-legacy-adapter\",\n version=\"1.0.0\",\n d"
},
{
"path": "kirara_ai/plugins/im_http_legacy_adapter/tests/api_test.py",
"chars": 3264,
"preview": "import asyncio\nimport os\nimport sys\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom kirara_ai.im.adapter "
},
{
"path": "kirara_ai/plugins/im_qqbot_adapter/__init__.py",
"chars": 1732,
"preview": "import asyncio\nimport os\n\nfrom im_qqbot_adapter.adapter import QQBotAdapter, QQBotConfig\n\nfrom kirara_ai.logger import g"
},
{
"path": "kirara_ai/plugins/im_qqbot_adapter/adapter.py",
"chars": 10642,
"preview": "import asyncio\nimport base64\nimport functools\nimport uuid\nfrom typing import List, Optional\n\nimport ymbotpy as botpy\nimp"
},
{
"path": "kirara_ai/plugins/im_qqbot_adapter/setup.py",
"chars": 388,
"preview": "from setuptools import find_packages, setup\n\nsetup(\n name=\"kirara_ai-qqbot-adapter\",\n version=\"1.0.0\",\n descrip"
},
{
"path": "kirara_ai/plugins/im_qqbot_adapter/utils.py",
"chars": 317,
"preview": "import re\n\nURL_PATTERN = re.compile(\n r'((?:https?://)?(?:[-\\w]+\\.)+(?:com|net|org|edu|gov|mil|io|co|ai|app|dev|cn|jp"
},
{
"path": "kirara_ai/plugins/im_telegram_adapter/__init__.py",
"chars": 1796,
"preview": "import asyncio\nimport os\n\nfrom im_telegram_adapter.adapter import TelegramAdapter, TelegramConfig\n\nfrom kirara_ai.logger"
},
{
"path": "kirara_ai/plugins/im_telegram_adapter/adapter.py",
"chars": 13896,
"preview": "import asyncio\nimport random\nfrom functools import lru_cache\nfrom typing import List, Optional\n\nfrom pydantic import Bas"
},
{
"path": "kirara_ai/plugins/im_telegram_adapter/setup.py",
"chars": 439,
"preview": "from setuptools import find_packages, setup\n\nsetup(\n name=\"kirara_ai-telegram-adapter\",\n version=\"1.0.0\",\n desc"
},
{
"path": "kirara_ai/plugins/im_wecom_adapter/__init__.py",
"chars": 1107,
"preview": "import os\n\nfrom kirara_ai.logger import get_logger\nfrom kirara_ai.plugin_manager.plugin import Plugin\nfrom kirara_ai.web"
},
{
"path": "kirara_ai/plugins/im_wecom_adapter/adapter.py",
"chars": 14927,
"preview": "import asyncio\nimport base64\nimport os\nimport uuid\nfrom io import BytesIO\nfrom typing import Any, List, Optional\n\nimport"
},
{
"path": "kirara_ai/plugins/im_wecom_adapter/delegates.py",
"chars": 4474,
"preview": "from abc import ABC, abstractmethod\nfrom io import BytesIO\nfrom typing import TYPE_CHECKING, Any\n\nfrom wechatpy.messages"
},
{
"path": "kirara_ai/plugins/im_wecom_adapter/setup.py",
"chars": 383,
"preview": "from setuptools import find_packages, setup\n\nsetup(\n name=\"kirara_ai-wecom-adapter\",\n version=\"1.0.0\",\n descrip"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/__init__.py",
"chars": 2667,
"preview": "from .alibabacloud_adapter import AlibabaCloudAdapter, AlibabaCloudConfig\nfrom .claude_adapter import ClaudeAdapter, Cla"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/alibabacloud_adapter.py",
"chars": 810,
"preview": "from kirara_ai.config.global_config import ModelConfig\n\nfrom .openai_adapter import OpenAIAdapter, OpenAIConfig\nfrom .ut"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/claude_adapter.py",
"chars": 7640,
"preview": "import asyncio\nimport base64\nfrom typing import Any, Dict, List\n\nimport aiohttp\nimport requests\nfrom pydantic import Bas"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/deepseek_adapter.py",
"chars": 280,
"preview": "from .openai_adapter import OpenAIAdapterChatBase, OpenAIConfig\n\n\nclass DeepSeekConfig(OpenAIConfig):\n api_base: str "
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/gemini_adapter.py",
"chars": 13036,
"preview": "import asyncio\nimport base64\nfrom typing import Any, Dict, List, Literal, cast\n\nimport aiohttp\nimport requests\nfrom pyda"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/minimax_adapter.py",
"chars": 276,
"preview": "from .openai_adapter import OpenAIAdapterChatBase, OpenAIConfig\n\n\nclass MinimaxConfig(OpenAIConfig):\n api_base: str ="
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/mistral_adapter.py",
"chars": 1626,
"preview": "import aiohttp\n\nfrom kirara_ai.llm.adapter import AutoDetectModelsProtocol\n\nfrom .openai_adapter import OpenAIAdapter, O"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/moonshot_adapter.py",
"chars": 409,
"preview": "from .openai_adapter import OpenAIAdapterChatBase, OpenAIConfig\n\n# https://platform.moonshot.cn/docs/intro#%E6%96%87%E6%"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/ollama_adapter.py",
"chars": 8294,
"preview": "import asyncio\nfrom typing import Any, List, cast\n\nimport aiohttp\nimport requests\nfrom pydantic import BaseModel, Config"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/openai_adapter.py",
"chars": 10824,
"preview": "import asyncio\nimport json\nfrom typing import Any, Dict, List, cast, Literal, TypedDict\n\nimport aiohttp\nimport requests\n"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/openrouter_adapter.py",
"chars": 1842,
"preview": "import aiohttp\n\nfrom kirara_ai.config.global_config import ModelConfig\nfrom kirara_ai.llm.model_types import LLMAbility,"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/setup.py",
"chars": 393,
"preview": "from setuptools import find_packages, setup\n\nsetup(\n name=\"kirara_ai-llm-presets\",\n version=\"1.0.0\",\n descripti"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/siliconflow_adapter.py",
"chars": 801,
"preview": "import aiohttp\n\nfrom .openai_adapter import OpenAIAdapter, OpenAIConfig\n\n\nclass SiliconFlowConfig(OpenAIConfig):\n api"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/tencentcloud_adapter.py",
"chars": 292,
"preview": "\nfrom .openai_adapter import OpenAIAdapter, OpenAIConfig\n\n\nclass TencentCloudConfig(OpenAIConfig):\n api_base: str = \""
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/tests/test_utils.py",
"chars": 11805,
"preview": "\nfrom llm_preset_adapters.utils import guess_openai_model\n\nfrom kirara_ai.llm.model_types import AudioModelAbility, Embe"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/utils.py",
"chars": 6490,
"preview": "import uuid\nfrom typing import Optional, Tuple\n\nfrom kirara_ai.llm.format.message import LLMChatContentPartType, LLMTool"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/volcengine_adapter.py",
"chars": 6801,
"preview": "import datetime\nimport hashlib\nimport hmac\nfrom urllib.parse import quote\n\nimport aiohttp\nfrom pydantic import Field\n\nfr"
},
{
"path": "kirara_ai/plugins/llm_preset_adapters/voyage_adapter.py",
"chars": 7748,
"preview": "from pydantic import BaseModel, ConfigDict\nfrom typing import cast, TypedDict, Literal, Optional\n\nimport requests\nimport"
},
{
"path": "kirara_ai/system/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "kirara_ai/system/updater.py",
"chars": 0,
"preview": ""
},
{
"path": "kirara_ai/tracing/__init__.py",
"chars": 375,
"preview": "from kirara_ai.tracing.core import TracerBase\nfrom kirara_ai.tracing.decorator import trace_llm_chat\nfrom kirara_ai.trac"
},
{
"path": "kirara_ai/tracing/core.py",
"chars": 6774,
"preview": "import abc\nimport asyncio\nimport uuid\nfrom asyncio import Queue\nfrom typing import Any, Dict, Generic, List, Optional, T"
},
{
"path": "kirara_ai/tracing/decorator.py",
"chars": 950,
"preview": "import functools\nfrom typing import Callable\n\nfrom kirara_ai.llm.format.request import LLMChatRequest\nfrom kirara_ai.llm"
},
{
"path": "kirara_ai/tracing/llm_tracer.py",
"chars": 12635,
"preview": "from datetime import datetime, timedelta\nfrom typing import Any, Dict\n\nfrom sqlalchemy import case, func\n\nfrom kirara_ai"
},
{
"path": "kirara_ai/tracing/manager.py",
"chars": 3424,
"preview": "import asyncio\nfrom typing import Dict, List, Optional\n\nfrom kirara_ai.database import DatabaseManager\nfrom kirara_ai.ev"
},
{
"path": "kirara_ai/tracing/models.py",
"chars": 4855,
"preview": "import json\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy import Column, DateTim"
},
{
"path": "kirara_ai/web/README.md",
"chars": 2808,
"preview": "# Web API 系统 🌐\n\n本系统提供了一套完整的RESTful API,用于管理和监控ChatGPT-Mirai机器人的各个组件。\n\n## 系统架构 🏗️\n\n- 基于 [Quart](https://pgjones.gitlab.io"
},
{
"path": "kirara_ai/web/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "kirara_ai/web/api/block/README.md",
"chars": 4589,
"preview": "# 区块 API 🧩\n\n区块 API 提供了查询工作流构建块类型的功能。每个区块类型定义了其输入、输出和配置项。\n\n>> 注意:文档由 Claude 生成,可能存在错误,请以实际代码为准。\n## API 端点\n\n### 获取所有区块类型\n\n"
},
{
"path": "kirara_ai/web/api/block/__init__.py",
"chars": 54,
"preview": "from .routes import block_bp\n\n__all__ = [\"block_bp\"]\n\n"
},
{
"path": "kirara_ai/web/api/block/diagnostics/base_diagnostic.py",
"chars": 6069,
"preview": "import ast\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Dict, List, Optional\n\nfrom lsprotocol.t"
},
{
"path": "kirara_ai/web/api/block/diagnostics/import_check.py",
"chars": 10926,
"preview": "import ast\nimport importlib.util\nimport logging\nimport os\nfrom typing import List, Optional, Tuple\n\nfrom lsprotocol.type"
},
{
"path": "kirara_ai/web/api/block/diagnostics/jedi_syntax_check.py",
"chars": 2799,
"preview": "import logging\nfrom typing import List\n\nimport jedi\nfrom lsprotocol.types import Diagnostic, DiagnosticSeverity, Positio"
},
{
"path": "kirara_ai/web/api/block/diagnostics/mandatory_function.py",
"chars": 11693,
"preview": "import ast\nimport logging\nfrom typing import Any, Dict, List, Optional\n\nfrom lsprotocol.types import (CodeAction, CodeAc"
},
{
"path": "kirara_ai/web/api/block/diagnostics/pyflakes_check.py",
"chars": 9277,
"preview": "import logging\nfrom typing import Any, List\n\nfrom lsprotocol.types import (CodeAction, CodeActionKind, CodeActionParams,"
},
{
"path": "kirara_ai/web/api/block/models.py",
"chars": 519,
"preview": "from typing import List\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.workflow.core.block.schema import BlockConfig, B"
},
{
"path": "kirara_ai/web/api/block/python_lsp.py",
"chars": 21297,
"preview": "import asyncio\nimport os\nfrom typing import Any, Dict, List, Optional, Union\n\nimport jedi\nfrom lsprotocol.types import ("
},
{
"path": "kirara_ai/web/api/block/routes.py",
"chars": 5091,
"preview": "import asyncio\nimport json\nfrom typing import Any\n\nfrom quart import Blueprint, g, jsonify, websocket\n\nfrom kirara_ai.lo"
},
{
"path": "kirara_ai/web/api/dispatch/README.md",
"chars": 4801,
"preview": "# 调度规则 API 📋\n\n调度规则 API 提供了消息处理规则的管理功能。调度规则决定了如何根据消息内容选择合适的工作流进行处理。\n\n## API 端点\n\n### 获取规则列表\n\n```http\nGET/backend-api/api/d"
},
{
"path": "kirara_ai/web/api/dispatch/__init__.py",
"chars": 60,
"preview": "from .routes import dispatch_bp\n\n__all__ = [\"dispatch_bp\"]\n\n"
},
{
"path": "kirara_ai/web/api/dispatch/models.py",
"chars": 306,
"preview": "from typing import List\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.workflow.core.dispatch import CombinedDispatchRu"
},
{
"path": "kirara_ai/web/api/dispatch/routes.py",
"chars": 5301,
"preview": "from quart import Blueprint, g, jsonify, request\n\nfrom kirara_ai.workflow.core.dispatch import CombinedDispatchRule, Dis"
},
{
"path": "kirara_ai/web/api/im/README.md",
"chars": 3393,
"preview": "# 即时通讯 API 🗨️\n\n即时通讯 API 提供了管理 IM 后端和适配器的功能。通过这些 API,你可以注册、配置和管理不同的 IM 平台适配器。\n\n## API 端点\n\n### 获取适配器类型\n\n```http\nGET/backen"
},
{
"path": "kirara_ai/web/api/im/__init__.py",
"chars": 48,
"preview": "from .routes import im_bp\n\n__all__ = [\"im_bp\"]\n\n"
},
{
"path": "kirara_ai/web/api/im/models.py",
"chars": 823,
"preview": "from typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.config.global_config import"
},
{
"path": "kirara_ai/web/api/im/routes.py",
"chars": 9279,
"preview": "import asyncio\n\nfrom quart import Blueprint, g, jsonify, request\n\nfrom kirara_ai.config.config_loader import CONFIG_FILE"
},
{
"path": "kirara_ai/web/api/llm/README.md",
"chars": 4984,
"preview": "# 大语言模型 API 🤖\n\n大语言模型 API 提供了管理 LLM 后端和适配器的功能。通过这些 API,你可以注册、配置和管理不同的大语言模型服务。\n\n## API 端点\n\n### 获取适配器类型\n\n```http\nGET/backen"
},
{
"path": "kirara_ai/web/api/llm/__init__.py",
"chars": 50,
"preview": "from .routes import llm_bp\n\n__all__ = [\"llm_bp\"]\n\n"
},
{
"path": "kirara_ai/web/api/llm/models.py",
"chars": 1079,
"preview": "from typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.config.global_config import"
},
{
"path": "kirara_ai/web/api/llm/routes.py",
"chars": 10052,
"preview": "from quart import Blueprint, g, jsonify, request\n\nfrom kirara_ai.config.config_loader import CONFIG_FILE, ConfigLoader\nf"
},
{
"path": "kirara_ai/web/api/mcp/README.md",
"chars": 3811,
"preview": "# MCP 服务器管理 API\n\nMCP(Model Context Protocol)是一种用于大型语言模型的通信协议。本 API 提供了管理 MCP 服务器的功能,包括创建、更新、删除、启动和停止服务器,以及获取服务器提供的工具列表。\n"
},
{
"path": "kirara_ai/web/api/mcp/__init__.py",
"chars": 49,
"preview": "from .routes import mcp_bp\n\n__all__ = [\"mcp_bp\"]\n"
},
{
"path": "kirara_ai/web/api/mcp/models.py",
"chars": 1371,
"preview": "from typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass MCPServerInfo(BaseModel):\n "
},
{
"path": "kirara_ai/web/api/mcp/routes.py",
"chars": 16217,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\nfrom typing import Any, Dict, Optional, cast\n\nfrom quart import Blueprin"
},
{
"path": "kirara_ai/web/api/media/README.md",
"chars": 2320,
"preview": "# 媒体管理API\n\n本模块提供了媒体文件管理的API,包括上传、查询、下载和删除媒体文件。\n\n## API接口\n\n### 获取媒体列表\n\n```\nPOST /api/media/list\n```\n\n请求体:\n```json\n{\n \"qu"
},
{
"path": "kirara_ai/web/api/media/__init__.py",
"chars": 53,
"preview": "from .routes import media_bp\n\n__all__ = [\"media_bp\"] "
},
{
"path": "kirara_ai/web/api/media/models.py",
"chars": 1088,
"preview": "from datetime import datetime\nfrom typing import List, Optional\n\nfrom pydantic import BaseModel\n\n\nclass MediaMetadata(Ba"
},
{
"path": "kirara_ai/web/api/media/routes.py",
"chars": 10225,
"preview": "import asyncio\nimport io\nimport os\nimport shutil\nimport time\nfrom typing import Optional\n\nimport pytz\nfrom quart import "
},
{
"path": "kirara_ai/web/api/plugin/README.md",
"chars": 4832,
"preview": "# 插件 API 🔌\n\n插件 API 提供了管理插件的功能。通过这些 API,你可以安装、卸载、启用、禁用和更新插件。\n\n## API 端点\n\n### 获取所有插件\n\n```http\nGET/backend-api/api/plugin/p"
},
{
"path": "kirara_ai/web/api/plugin/__init__.py",
"chars": 56,
"preview": "from .routes import plugin_bp\n\n__all__ = [\"plugin_bp\"]\n\n"
},
{
"path": "kirara_ai/web/api/plugin/models.py",
"chars": 412,
"preview": "from typing import List, Optional\n\nfrom pydantic import BaseModel\n\nfrom kirara_ai.plugin_manager.models import PluginInf"
},
{
"path": "kirara_ai/web/api/plugin/routes.py",
"chars": 9253,
"preview": "from functools import lru_cache\n\nimport aiohttp\nfrom packaging.version import Version\nfrom quart import Blueprint, g, js"
}
]
// ... and 138 more files (download for full content)
About this extraction
This page contains the full source code of the lss233/kirara-ai GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 338 files (1.1 MB), approximately 288.7k tokens, and a symbol index with 2024 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.