diff --git a/.spec-coding/features/anthropic-agent-skills/README.md b/.spec-coding/features/anthropic-agent-skills/README.md new file mode 100644 index 000000000..65c93f0e8 --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/README.md @@ -0,0 +1,38 @@ +# Anthropic Agent Skills 需求文档 + +## 1. 概述 +在 BotSharp 中实现 Antrhopic 推出的 "Agent Skills" 标准。该标准采用“文件系统中心化”设计和“渐进式披露”范式。Agent 不会在初始阶段加载所有工具,而是根据任务需求动态发现、加载并执行能力。 + +## 2. 核心组件 (基于文件系统的技能单元) +每个 Skill 是一个独立的目录,包含以下核心文件: +* **`SKILL.md`**: 核心定义文件。 + * **YAML Frontmatter**: 包含 `name` (唯一标识) 和 `description` (用于发现)。 + * **Markdown Body**: 包含详细的 SOP (标准作业程序/指令)。这部分仅在加载后对 LLM 可见。 +* **`scripts/`**: 包含可执行逻辑的代码文件 (如 `.py`, `.sh`)。 +* **`resources/`**: (可选) 静态资源文件。 + +## 3. 交互生命周期 (The Progressive Paradigm) +系统需支持以下四个关键阶段: + +1. **索引 (Indexing)**: + * 系统需扫描指定目录下所有的 Skill 文件夹。 + * 解析每个 Skill 的 `SKILL.md` 中的 YAML 头信息。 + +2. **感知 (Awareness)**: + * 将所有已发现 Skill 的 `name` 和 `description` 注入到 Agent 的初始 System Prompt 中。 + * 目的:让 LLM 知道有哪些技能“可用”,但不知道具体的指令细节。 + +3. **加载 (Loading)**: + * **机制**: 提供内置工具 `load_skill(skill_name)` (或类似命名)。 + * **流程**: 当 LLM 决定使用某技能并调用此工具时,系统读取对应 `SKILL.md` 的 Markdown 正文。 + * **结果**: 将正文内容追加到当前的对话上下文中 (Context),使 LLM 获得执行该任务的详细 SOP。 + +4. **调用 (Invocation)**: + * **机制**: 提供内置工具 `run_skill_script(skill_name, script_file, args)` (方案A)。 + * **流程**: LLM 根据 SOP 中的指示,构造参数调用此工具。 + * **执行**: 系统在宿主环境中执行对应的 `scripts/` 下的代码文件 (支持 Python 等),并将标准输出作为结果返回给 LLM。 + +## 4. 技术约束 +* **插件化**: 实现为 `BotSharp.Plugin.AgentSkills`。 +* **脚本执行**: 需复用或扩展现有的 Python 执行能力,支持“预定义代码”的运行,而非仅支持 REPL 模式。 +* **配置**: 需支持配置 Skill 库的根路径。 diff --git a/.spec-coding/features/anthropic-agent-skills/design.md b/.spec-coding/features/anthropic-agent-skills/design.md new file mode 100644 index 000000000..9b14772ea --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/design.md @@ -0,0 +1,203 @@ +# Anthropic Agent Skills 设计文档 (Design) + +## 1. 概述 (Overview) +本设计旨在 BotSharp 中实现 Agent Skills 标准。通过一个新的插件 `BotSharp.Plugin.AgentSkills`,提供技能发现、动态加载和脚本执行的能力。该设计遵循“渐进式披露”原则,降低 Agent 的初始上下文负载。 + +## 2. 架构 (Architecture) +架构主要由以下几个核心组件组成: + +1. **AgentSkillPlugin**: 插件入口,注册服务、配置和钩子。 +2. **AgentSkillService**: 核心服务,负责扫描文件系统、解析 `SKILL.md`、缓存技能元数据。 +3. **AgentSkillHook**: 实现 `IAgentHook`,在 Agent 加载阶段注入可用技能的摘要(Awareness)。 +4. **Function Tools**: + * `LoadSkillFn`: 实现 `IFunctionCallback`,用于动态加载技能详情。 + * `RunSkillScriptFn`: 实现 `IFunctionCallback`,用于执行技能脚本。 +5. **ScriptExecutor**: 负责安全地在宿主环境中执行脚本(主要是 Python)。 + +### 2.1 依赖关系 +* `BotSharp.Abstraction`: 核心接口 (`IBotSharpPlugin`, `IAgentHook`, `IFunctionCallback`). +* `BotSharp.Plugin.PythonInterpreter`: 用于执行 Python 脚本。需要扩展以支持执行预定义文件。 +* `YamlDotNet`: 用于解析 YAML Frontmatter。 +* `System.Diagnostics.Process`: 用于执行外部脚本。 + +## 3. 组件与接口 (Components & Interfaces) + +### 3.1 数据模型 (Models) + +为了准确解析 `SKILL.md` 的 YAML Frontmatter,我们需要定义对应的强类型模型: + +```csharp +public class AgentSkill +{ + public string Name { get; set; } + public string Description { get; set; } + public string MarkdownBody { get; set; } // 正文指令内容 + public string BaseDir { get; set; } // 技能所在的物理目录路径 + public List Scripts { get; set; } // 可执行脚本列表 + public List Resources { get; set; } // 静态资源列表 +} + +/// +/// 用于 YAML 反序列化的中间模型 +/// +public class SkillFrontmatter +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "description")] + public string Description { get; set; } +} + +public class AgentSkillsSettings +{ + public string DataDir { get; set; } = "skills"; +} +``` + +### 3.2 核心服务 (AgentSkillService) + +`AgentSkillService` 应该注册为 **Singleton**,以维护技能缓存并处理热重载。 + +* **职责**: + 1. 启动时递归扫描配置的 Skills 目录。 + 2. 解析 `SKILL.md` 并构建 `AgentSkill` 对象。 + 3. 维护 `ConcurrentDictionary` 缓存。 + 4. 使用 `FileSystemWatcher` 监听 `*.md` 文件的变更,实现 Hot Reload。 + +```csharp +public interface IAgentSkillService +{ + // 扫描并返回所有可用技能的摘要(Name & Description) + Task> GetAvailableSkills(); + + // 获取特定技能的详细信息(包含 MarkdownBody) + Task GetSkill(string name); + + // 验证并获取脚本路径 + string GetScriptPath(string skillName, string scriptFile); + + // 手动刷新缓存(可选,配合 Watcher 使用) + Task RefreshSkills(); +} +``` + +### 3.3 生命周期钩子 (AgentSkillHook) (实现细节) + +**阶段一:发现 (Discovery)** +* **Hook**: `OnInstructionLoaded` +* **逻辑**: + 1. 调用 `IAgentSkillService.GetAvailableSkills()`。 + 2. 构造“技能菜单”提示词: + ```text + You have access to the following specialized skills. If a task requires one, call the 'load_skill' function with the skill name. + + - pdf-processing: Handles PDF files... + - data-analysis: Performs statistical analysis... + ``` + 3. 此段提示词作为 System Instruction 的一部分注入。 + +**阶段二:激活 (Activation)** +* **Hook**: `OnInstructionLoaded` (再次触发时) +* **逻辑**: + 1. 检查 `conversation_state` 中的 `active_skills` 列表。 + 2. 如果发现已激活的技能(例如 "pdf-processing"),从 `AgentSkillService` 获取其 `MarkdownBody`。 + 3. 将正文注入 Prompt: + ```text + ### ACTIVE SKILL: PDF PROCESSING + (Markdown content...) + ``` + +**阶段三:执行 (Execution)** +* **Hook**: `OnFunctionsLoaded` +* **逻辑**: + 1. 虽然 `run_skill_script` 是通用函数,但可以在此 Hook 中做权限校验或动态提示。 + 2. 确保 `load_skill` 和 `run_skill_script` 工具已添加到当前 Agent 的功能列表中。 + 3. (可选) 遍历 `active_skills`,显式提示用户当前可执行的脚本列表(虽然主要依靠 Markdown Body 中的 SOP 指引)。 + +### 3.4 工具 (Functions) + +#### LoadSkillFn +* **Name**: `load_skill` +* **Logic**: + 1. 接收 `skill_name`。 + 2. 更新 `conversation_state["active_skills"]`,加入该技能。 + 3. **关键**: 返回提示 "Skill '{skill_name}' activated. Please continue.",促使 LLM 在下一轮对话中利用新注入的 Prompt(因 `OnInstructionLoaded` 会在每轮生成 Prompt 时运行)。 + +#### RunSkillScriptFn +* **Name**: `run_skill_script` +* **Input**: `skill_name`, `script_file`, `args` (string/json) +* **Logic**: + 1. 验证 `skill_name` 和 `script_file` 合法性(防止目录遍历)。 + 2. 定位物理文件路径。 + 3. 启动 `python` 进程执行脚本。 + 4. 捕获 Standard Output 和 Standard Error。 + +### 3.5 脚本执行增强 (BotSharp.Plugin.PythonInterpreter Extension) + +为了支持执行预定义的本地 Python 文件,我们需要扩展 `PyCodeInterpreter` 或提供新的服务接口。 + +* **新增接口**: `IPyScriptExecutor` (或在 `ICodeProcessor` 中增加方法) + ```csharp + public interface IPyScriptRunner + { + // 直接运行指定路径的 py 文件,并传入参数 + Task RunScript(string filePath, string args); + } + ``` +* **实现逻辑**: + * 复用现有的 Python 环境配置 (`PythonInterpreterSettings`)。 + * 使用 `Process.Start` 启动 python 进程。 + * Argument 传递:`python "path/to/script.py" "args_string"`。 + * 在 `AgentSkill` 插件中,通过 DI 获取此服务来执行 `RunSkillScriptFn` 中的请求。 + +## 4. 流程设计 (Flows) + +### 4.1 总体交互流程 + +```mermaid +sequenceDiagram + participant User + participant Agent + participant SkillHook + participant SkillService + participant LoadFn + participant RunFn + participant FileSystem + + User->>Agent: "Start conversation" + Agent->>SkillHook: OnInstructionLoaded + SkillHook->>SkillService: GetAvailableSkills() + SkillService->>FileSystem: Scan SKILL.md + SkillService-->>SkillHook: List + SkillHook-->>Agent: Inject "Available Skills..." + + User->>Agent: "Help me process this PDF..." + Agent->>Agent: Decide to use 'pdf-processing' + Agent->>LoadSkillFn: Call load_skill("pdf-processing") + LoadSkillFn->>SkillService: GetSkill("pdf-processing") + SkillService->>FileSystem: Read Markdown Body + LoadSkillFn-->>Agent: Return Instructions (SOP) + + Agent->>Agent: Read SOP, decide to run script + Agent->>RunSkillScriptFn: Call run_skill_script("pdf-processing", "analyze.py", ...) + RunSkillScriptFn->>FileSystem: Execute script + FileSystem-->>RunSkillScriptFn: StdOut / StdErr + RunSkillScriptFn-->>Agent: Return Result + Agent-->>User: "Here is the result..." +``` + +## 5. 错误处理 (Error Handling) +* **文件未找到**: 如果加载不存在的 Skill,返回明确错误 "Skill not found". +* **脚本安全**: + * 严禁 `script_file` 包含 `..` 或绝对路径。 + * 仅允许执行 `scripts/` 子目录下的文件。 +* **执行失败**: 脚本退出码非 0 时,捕获 Stderr 并以 "Execution Failed: ..." 格式返回。 + +## 6. 测试策略 (Testing Strategy) +* **单元测试**: 针对 `AgentSkillService` 的 YAML 解析逻辑。 +* **集成测试**: + * 构建一个真实的 `skills/test-skill` 目录。 + * 模拟 Agent 调用流程,验证 System Prompt 注入是否正确。 + * 验证 `load_skill` 是否返回各个 section。 + * 验证 `run_skill_script` 能否执行简单的 python print 脚本。 diff --git a/.spec-coding/features/anthropic-agent-skills/requirements.md b/.spec-coding/features/anthropic-agent-skills/requirements.md new file mode 100644 index 000000000..4592a5afd --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/requirements.md @@ -0,0 +1,43 @@ +# Anthropic Agent Skills 需求文档 (Requirements) + +## 1. 概述 (Overview) +本功能旨在为 BotSharp 引入 Anthropic 的 "Agent Skills" 标准支持。通过实现一个新的插件 `BotSharp.Plugin.AgentSkills`,允许 Agent 采用“渐进式披露”(Progressive Disclosure)模式进行交互。与传统的一次性加载所有工具不同,Agent 将首先感知可用技能的摘要,根据需要“加载”特定技能的详细指令,并通过通用接口“执行”技能中定义的脚本。 + +## 2. 需求列表 (Requirements) + +### 2.1 技能索引与感知 (Indexing & Awareness) +**用户故事**: 作为 BotSharp 管理员,我希望系统能自动发现指定目录下的技能,以便 Agent 在初始状态下知道有哪些能力可用,而无需预加载所有繁重的指令。 + +* **REQ-001**: 当 Agent 初始化或系统启动时,组件 **必须** 扫描配置的技能根目录。 +* **REQ-002**: 当发现从属目录中包含 `SKILL.md` 时,系统 **必须** 解析其 YAML Frontmatter 以提取 `name` 和 `description`。 +* **REQ-003**: 如果 YAML 解析成功,系统 **必须** 将格式化后的技能列表(包含名称和描述)注入到 Agent 的 System Prompt(或可用的 Context Window)中。 +* **REQ-004**: 如果 `SKILL.md` 格式无效或元数据缺失,系统 **应该** 记录警告日志并跳过该技能,即不中断整体启动流程。 + +### 2.2 技能加载 (Skill Loading) +**用户故事**: 作为 LLM Agent,我希望在判断某个技能对当前任务有用时能够动态加载其详细指令,以便获取执行任务所需的标准作业程序(SOP)。 + +* **REQ-005**: 系统 **必须** 向 LLM 暴露一个名为 `load_skill`(或语义等效)的 Function Tool。 +* **REQ-006**: 当 `load_skill` 被调用且 `skill_name` 有效时,系统 **必须** 读取对应 `SKILL.md` 的 Markdown 正文(Body)。 +* **REQ-007**: 当获取到 Markdown 正文后,系统 **必须** 将其作为消息(System 或 User 角色)追加到当前的对话上下文中,使其对后续推理可见。 +* **REQ-008**: 如果请求的 `skill_name` 不存在,工具调用 **必须** 返回明确的错误提示给 Agent。 + +### 2.3 技能脚本执行 (Skill Invocation) +**用户故事**: 作为 LLM Agent,我希望能通过一个通用的执行接口运行技能包中定义的脚本,以便按照 SOP 完成具体的操作。 + +* **REQ-009**: 系统 **必须** 向 LLM 暴露一个名为 `run_skill_script` 的 Function Tool,接受 `skill_name`, `script_file`, `args` 等参数。 +* **REQ-010**: 当 `run_skill_script` 被调用时,系统 **必须** 验证请求的脚本文件是否存在于该技能的 `scripts/` 子目录下,防止路径遍历攻击。 +* **REQ-011**: 如果脚本是 Python 文件(`.py`),系统 **必须** 调用宿主环境的 Python 解释器执行该脚本,并将 `args` 传递给进程。 +* **REQ-012**: 当脚本执行完成,系统 **必须** 捕获标准输出(Stdout)作为工具的成功返回值。 +* **REQ-013**: 如果脚本执行失败(退出码非0或抛出异常),系统 **必须** 捕获错误输出(Stderr)并将其包装为错误信息返回给 Agent。 + +### 2.4 配置与扩展性 (Configuration & Extensibility) +**用户故事**: 作为开发者,我希望能够灵活配置技能库的位置,并以插件形式集成此功能。 + +* **REQ-014**: 系统 **必须** 利用 BotSharp 的配置机制(如 `appsettings.json`)读取技能库的根路径(例如 `AgentSkills:DataDir`)。 +* **REQ-015**: 如果未提供配置,系统 **应该** 默认使用应用工作目录下的 `skills` 文件夹。 +* **REQ-016**: 所有功能 **必须** 封装在 `BotSharp.Plugin.AgentSkills` 项目中,通过实现 `IBotSharpPlugin` 接口进行服务注册。 + +## 3. 验收标准示例 (Acceptance Criteria Example) +* **Case 1: 发现技能**: 在 `skills/pdf-processing/SKILL.md` 存在的情况下,启动 Agent,System Prompt 中应包含 "pdf-processing" 及其描述。 +* **Case 2: 动态加载**: 对 Agent 说 "我要处理 PDF",Agent 调用 `load_skill("pdf-processing")`,随后的 Prompt 中包含了 PDF 处理的具体步骤。 +* **Case 3: 脚本执行**: Agent 调用 `run_skill_script("pdf-processing", "analyze.py", ...)`,系统成功执行本地 Python 脚本并返回结果字符串。 diff --git a/.spec-coding/features/anthropic-agent-skills/tasks.md b/.spec-coding/features/anthropic-agent-skills/tasks.md new file mode 100644 index 000000000..d5179cb46 --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/tasks.md @@ -0,0 +1,30 @@ +# Implementation Tasks - Anthropic Agent Skills + +- [x] **Infrastructure - Create Plugin Project**: Create `BotSharp.Plugin.AgentSkills` project and register it in `BotSharp.csproj`. +- [x] **Infrastructure - Define Data Models**: Create `AgentSkill` and `SkillFrontmatter` models to map `SKILL.md` structure. +- [x] **Core Service - Implement AgentSkillService**: + - [x] Create `IAgentSkillService` and `AgentSkillService`. + - [x] Implement directory scanning and `SKILL.md` YAML parsing using `YamlDotNet`. + - [x] Implement `GetAvailableSkills` and `GetSkill` methods. + - [x] Implement `FileSystemWatcher` for hot reload support. +- [x] **Hooks - Implement AgentSkillHook**: + - [x] Create `AgentSkillHook` inheriting `AgentHookBase`. + - [x] Implement `OnInstructionLoaded` for "Discovery" phase (inject available skills list). + - [x] Implement logic to inject active skill instructions for "Activation" phase. +- [x] **Functions - Implement LoadSkillFn**: + - [x] Create `LoadSkillFn` implementing `IFunctionCallback`. + - [x] Implement logic to add skill to `active_skills` in conversation state. + - [x] Return appropriate message to prompt LLM for next step. +- [x] **Execution - Extend Python Interpreter**: + - [x] Define `IPyScriptRunner` interface in `BotSharp.Plugin.PythonInterpreter`. + - [x] Implement `PyScriptRunner` to execute local `.py` files using `Process.Start`. + - [x] Register the new service in `PythonInterpreterPlugin`. +- [x] **Functions - Implement RunSkillScriptFn**: + - [x] Create `RunSkillScriptFn` implementing `IFunctionCallback`. + - [x] Implement parameter validation (path security). + - [x] Use `IPyScriptRunner` to execute the requested script. +- [x] **Testing - Integration**: + - [x] Create a sample `skills/demo-skill` structure. + - [x] Verify discovery (System Prompt injection). + - [x] Verify loading (Context injection). + - [x] Verify execution (Script result). diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..f5df4a814 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "aaron-bond.better-comments", + "codezombiech.gitignore", + "cpylua.language-postcss", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "foxundermoon.shell-format", + "jasonnutter.search-node-modules", + "jock.svg", + "mikestead.dotenv", + "streetsidesoftware.code-spell-checker", + "stylelint.vscode-stylelint" + ], + "unwantedRecommendations": [ + "nucllear.vscode-extension-auto-import", + "steoates.autoimport" + ] +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..e3ea61c6b --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,13 @@ +{ + "servers": { + "SpecCodingMcpServer": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "C:\\workshop\\ai4c\\speccodingmcpserver\\src\\SpecCodingMcpServer\\SpecCodingMcpServer.csproj" + ] + } + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0fc8a800d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,182 @@ +{ + "eslint.nodePath": "frontend/config/eslint-config/node_modules/eslint", + "prettier.prettierPath": "frontend/config/eslint-config/node_modules/prettier", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnType": false, + "editor.formatOnPaste": false, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "never", + "source.fixAll.eslint": "never", + "source.removeUnused": "never", + "source.organizeImports": "never" + }, + "editor.formatOnSaveMode": "modificationsIfAvailable", + "search.followSymlinks": false, + "search.exclude": { + "**/node_modules": true, + "**/.nyc_output": true, + "**/.rush": true, + "**/pnpm-lock.yaml": true, + "**/CHANGELOG.json": true, + "**/CHANGELOG.md": true, + "common/changes": true, + "**/output": true, + "**/lib": true, + "**/rush-logs": true, + "**/dist": true, + "**/coverage": true, + "common/temp": true + }, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "files.defaultLanguage": "plaintext", + "files.associations": { + ".code-workspace": "jsonc", + ".babelrc": "json", + ".eslintrc": "jsonc", + ".eslintrc*.json": "jsonc", + ".stylelintrc": "javascript", + "stylelintrc": "jsonc", + "*.json": "jsonc", + "package.json": "json", + ".htmlhintrc": "jsonc", + "htmlhintrc": "jsonc", + "Procfile*": "shellscript", + "README": "markdown", + "**/coverage/**/*.*": "plaintext", + "OWNERS": "yaml", + "**/pnpm-lock.yaml": "plaintext", + "**/dist/**": "plaintext", + "**/dist_*/**": "plaintext", + "*.map": "plaintext", + "*.log": "plaintext" + }, + "files.exclude": { + "**/.git": true, + "**/rush-logs": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.rush": true, + "**/.swc": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/*/**": true + }, + "search.useIgnoreFiles": true, + // + "editor.rulers": [80, 120], + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "cSpell.diagnosticLevel": "Warning", + "eslint.probe": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.semanticHighlighting.enabled": false, + "eslint.format.enable": false, + "eslint.enable": true, + "eslint.useFlatConfig": true, + "eslint.codeActionsOnSave.mode": "problems", + "eslint.lintTask.enable": false, + "javascript.validate.enable": false, + "typescript.tsdk": "config/ts-config/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.format.enable": false, + "typescript.referencesCodeLens.enabled": false, + "typescript.preferGoToSourceDefinition": true, + "typescript.updateImportsOnFileMove.enabled": "never", + "typescript.tsserver.log": "off", + "typescript.tsserver.experimental.enableProjectDiagnostics": false, + "typescript.workspaceSymbols.excludeLibrarySymbols": true, + "editor.minimap.enabled": true, + "typescript.preferences.includePackageJsonAutoImports": "off", + "typescript.suggest.autoImports": true, + "typescript.tsserver.maxTsServerMemory": 10240, + "typescript.tsserver.enableRegionDiagnostics": false, + "typescript.tsserver.watchOptions": { + "fallbackPolling": "dynamicPriorityPolling", + "synchronousWatchDirectory": true, + "watchDirectory": "useFsEvents", + "watchFile": "useFsEventsOnParentDirectory", + "excludeDirectories": ["/**/node_modules"], + "excludeLibrarySymbols": true, + "excludeFiles": ["/**/node_modules/**"] + }, + "css.validate": false, + "scss.validate": false, + "less.validate": false, + "stylelint.enable": true, + "stylelint.validate": ["css", "scss", "less"], + "stylelint.stylelintPath": "frontend/config/stylelint-config/node_modules/stylelint", + "emmet.triggerExpansionOnTab": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[less]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[ignore]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[dotenv]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[svg]": { + "editor.defaultFormatter": "jock.svg" + }, + "[xml]": { + "editor.defaultFormatter": "mblode.pretty-formatter" + }, + "[sql]": { + "editor.defaultFormatter": "adpyke.vscode-sql-formatter" + }, + "svg.preview.background": "white", + "git.openRepositoryInParentFolders": "always", + "references.preferredLocation": "view", + "makefile.configureOnOpen": false +} diff --git a/BotSharp.sln b/BotSharp.sln index f6a6f5d93..7fbc908ae 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -145,500 +145,786 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{242F2D93-FCCE-4982-8075-F3052ECCA92C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", "src\Plugins\BotSharp.Plugin.GiteeAI\BotSharp.Plugin.GiteeAI.csproj", "{50B57066-3267-1D10-0F72-D2F5CC494F2C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", "src\Plugins\BotSharp.Plugin.Membase\BotSharp.Plugin.Membase.csproj", "{13223C71-9EAC-9835-28ED-5A4833E6F915}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{C548FDFF-B882-B552-D428-5C8EC4478187}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.AgentSkills", "src\Plugins\BotSharp.Plugin.AgentSkills\BotSharp.Plugin.AgentSkills.csproj", "{54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "tests\UnitTest\UnitTest.csproj", "{5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skills", "Skills", "{416C0757-85C6-430B-B973-B72FCDC7EB94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x64.ActiveCfg = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x64.Build.0 = Debug|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x86.Build.0 = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|Any CPU.Build.0 = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x64.ActiveCfg = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x64.Build.0 = Release|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x86.ActiveCfg = Release|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x86.Build.0 = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|Any CPU.Build.0 = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x64.ActiveCfg = Debug|x64 {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x64.Build.0 = Debug|x64 + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x86.ActiveCfg = Debug|Any CPU + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x86.Build.0 = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|Any CPU.ActiveCfg = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|Any CPU.Build.0 = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x64.ActiveCfg = Release|x64 {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x64.Build.0 = Release|x64 + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x86.ActiveCfg = Release|Any CPU + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x86.Build.0 = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|Any CPU.Build.0 = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x64.ActiveCfg = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x64.Build.0 = Debug|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x86.ActiveCfg = Debug|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x86.Build.0 = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|Any CPU.ActiveCfg = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|Any CPU.Build.0 = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x64.ActiveCfg = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x64.Build.0 = Release|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x86.ActiveCfg = Release|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x86.Build.0 = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|Any CPU.Build.0 = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x64.ActiveCfg = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x64.Build.0 = Debug|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x86.Build.0 = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|Any CPU.Build.0 = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x64.ActiveCfg = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x64.Build.0 = Release|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x86.ActiveCfg = Release|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x86.Build.0 = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|Any CPU.Build.0 = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x64.ActiveCfg = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x64.Build.0 = Debug|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x86.ActiveCfg = Debug|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x86.Build.0 = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|Any CPU.ActiveCfg = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|Any CPU.Build.0 = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x64.ActiveCfg = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x64.Build.0 = Release|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x86.ActiveCfg = Release|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x86.Build.0 = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|Any CPU.Build.0 = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x64.ActiveCfg = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x64.Build.0 = Debug|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x86.ActiveCfg = Debug|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x86.Build.0 = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|Any CPU.ActiveCfg = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|Any CPU.Build.0 = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x64.ActiveCfg = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x64.Build.0 = Release|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x86.ActiveCfg = Release|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x86.Build.0 = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x64.ActiveCfg = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x64.Build.0 = Debug|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x86.Build.0 = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|Any CPU.Build.0 = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x64.ActiveCfg = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x64.Build.0 = Release|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x86.ActiveCfg = Release|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x86.Build.0 = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x64.ActiveCfg = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x64.Build.0 = Debug|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x86.Build.0 = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|Any CPU.Build.0 = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x64.ActiveCfg = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x64.Build.0 = Release|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x86.ActiveCfg = Release|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x86.Build.0 = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x64.ActiveCfg = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x64.Build.0 = Debug|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x86.Build.0 = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|Any CPU.Build.0 = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x64.ActiveCfg = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x64.Build.0 = Release|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.ActiveCfg = Release|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.Build.0 = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x64.ActiveCfg = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x64.Build.0 = Debug|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x86.ActiveCfg = Debug|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x86.Build.0 = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|Any CPU.Build.0 = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x64.ActiveCfg = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x64.Build.0 = Release|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x86.ActiveCfg = Release|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x86.Build.0 = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x64.ActiveCfg = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x64.Build.0 = Debug|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x86.Build.0 = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|Any CPU.Build.0 = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x64.ActiveCfg = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x64.Build.0 = Release|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x86.ActiveCfg = Release|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x86.Build.0 = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x64.ActiveCfg = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x64.Build.0 = Debug|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x86.Build.0 = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|Any CPU.Build.0 = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x64.ActiveCfg = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x64.Build.0 = Release|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x86.ActiveCfg = Release|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x86.Build.0 = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|Any CPU.Build.0 = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x64.ActiveCfg = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x64.Build.0 = Debug|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x86.ActiveCfg = Debug|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x86.Build.0 = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|Any CPU.ActiveCfg = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|Any CPU.Build.0 = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x64.ActiveCfg = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x64.Build.0 = Release|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x86.ActiveCfg = Release|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x86.Build.0 = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x64.ActiveCfg = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x64.Build.0 = Debug|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x86.Build.0 = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|Any CPU.Build.0 = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x64.ActiveCfg = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x64.Build.0 = Release|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x86.ActiveCfg = Release|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x86.Build.0 = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|Any CPU.Build.0 = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x64.ActiveCfg = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x64.Build.0 = Debug|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x86.ActiveCfg = Debug|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x86.Build.0 = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|Any CPU.ActiveCfg = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|Any CPU.Build.0 = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x64.ActiveCfg = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x64.Build.0 = Release|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x86.ActiveCfg = Release|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x86.Build.0 = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x64.ActiveCfg = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x64.Build.0 = Debug|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x86.Build.0 = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|Any CPU.Build.0 = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x64.ActiveCfg = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x64.Build.0 = Release|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x86.ActiveCfg = Release|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x86.Build.0 = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x64.ActiveCfg = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x64.Build.0 = Debug|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x86.Build.0 = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|Any CPU.Build.0 = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x64.ActiveCfg = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x64.Build.0 = Release|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x86.ActiveCfg = Release|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x86.Build.0 = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x64.ActiveCfg = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x64.Build.0 = Debug|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x86.Build.0 = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|Any CPU.Build.0 = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.ActiveCfg = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.Build.0 = Release|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x86.ActiveCfg = Release|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x86.Build.0 = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.ActiveCfg = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.Build.0 = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x86.Build.0 = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x86.ActiveCfg = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x86.Build.0 = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.Build.0 = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.ActiveCfg = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.Build.0 = Debug|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x86.ActiveCfg = Debug|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x86.Build.0 = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.Build.0 = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.ActiveCfg = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.Build.0 = Release|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x86.ActiveCfg = Release|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x86.Build.0 = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.Build.0 = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x86.Build.0 = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.Build.0 = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.ActiveCfg = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.Build.0 = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x86.ActiveCfg = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x86.Build.0 = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x64.ActiveCfg = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x64.Build.0 = Debug|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x86.Build.0 = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|Any CPU.Build.0 = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x64.ActiveCfg = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x64.Build.0 = Release|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x86.ActiveCfg = Release|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x86.Build.0 = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|Any CPU.Build.0 = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x64.ActiveCfg = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x64.Build.0 = Debug|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x86.ActiveCfg = Debug|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x86.Build.0 = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|Any CPU.ActiveCfg = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|Any CPU.Build.0 = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x64.ActiveCfg = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x64.Build.0 = Release|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x86.ActiveCfg = Release|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x86.Build.0 = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x64.ActiveCfg = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x64.Build.0 = Debug|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x86.Build.0 = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|Any CPU.Build.0 = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x64.ActiveCfg = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x64.Build.0 = Release|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x86.ActiveCfg = Release|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x86.Build.0 = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x64.ActiveCfg = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x64.Build.0 = Debug|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x86.Build.0 = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|Any CPU.Build.0 = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x64.ActiveCfg = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x64.Build.0 = Release|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x86.ActiveCfg = Release|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x86.Build.0 = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x64.ActiveCfg = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x64.Build.0 = Debug|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x86.Build.0 = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|Any CPU.Build.0 = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x64.ActiveCfg = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x64.Build.0 = Release|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x86.ActiveCfg = Release|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x86.Build.0 = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|Any CPU.Build.0 = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x64.ActiveCfg = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x64.Build.0 = Debug|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x86.ActiveCfg = Debug|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x86.Build.0 = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|Any CPU.ActiveCfg = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|Any CPU.Build.0 = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x64.ActiveCfg = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x64.Build.0 = Release|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x86.ActiveCfg = Release|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x86.Build.0 = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|Any CPU.Build.0 = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x64.ActiveCfg = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x64.Build.0 = Debug|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x86.ActiveCfg = Debug|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x86.Build.0 = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|Any CPU.ActiveCfg = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|Any CPU.Build.0 = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|x64.ActiveCfg = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|x64.Build.0 = Release|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Release|x86.ActiveCfg = Release|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Release|x86.Build.0 = Release|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x64.ActiveCfg = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x64.Build.0 = Debug|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x86.Build.0 = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|Any CPU.Build.0 = Release|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x64.ActiveCfg = Release|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x64.Build.0 = Release|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x86.ActiveCfg = Release|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x86.Build.0 = Release|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x64.ActiveCfg = Debug|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x64.Build.0 = Debug|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x86.Build.0 = Debug|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|Any CPU.Build.0 = Release|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x64.ActiveCfg = Release|Any CPU {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x64.Build.0 = Release|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x86.ActiveCfg = Release|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x86.Build.0 = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x64.ActiveCfg = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x64.Build.0 = Debug|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x86.Build.0 = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.Build.0 = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.ActiveCfg = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.Build.0 = Release|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x86.ActiveCfg = Release|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x86.Build.0 = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.ActiveCfg = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.Build.0 = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x86.Build.0 = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.Build.0 = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.ActiveCfg = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.Build.0 = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x86.ActiveCfg = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x86.Build.0 = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|Any CPU.Build.0 = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x64.ActiveCfg = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x64.Build.0 = Debug|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x86.ActiveCfg = Debug|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x86.Build.0 = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|Any CPU.ActiveCfg = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|Any CPU.Build.0 = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x64.ActiveCfg = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x64.Build.0 = Release|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x86.ActiveCfg = Release|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x86.Build.0 = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x64.ActiveCfg = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x64.Build.0 = Debug|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x86.Build.0 = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|Any CPU.Build.0 = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x64.ActiveCfg = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x64.Build.0 = Release|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x86.ActiveCfg = Release|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x86.Build.0 = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x64.ActiveCfg = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x64.Build.0 = Debug|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x86.Build.0 = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|Any CPU.Build.0 = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x64.ActiveCfg = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x64.Build.0 = Release|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x86.ActiveCfg = Release|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x86.Build.0 = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x64.ActiveCfg = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x64.Build.0 = Debug|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x86.Build.0 = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|Any CPU.Build.0 = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.ActiveCfg = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.Build.0 = Release|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x86.ActiveCfg = Release|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x86.Build.0 = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.ActiveCfg = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.Build.0 = Debug|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x86.Build.0 = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.Build.0 = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.ActiveCfg = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.Build.0 = Release|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x86.ActiveCfg = Release|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x86.Build.0 = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x64.ActiveCfg = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x64.Build.0 = Debug|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x86.Build.0 = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.Build.0 = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.ActiveCfg = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.Build.0 = Release|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x86.ActiveCfg = Release|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x86.Build.0 = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.ActiveCfg = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.Build.0 = Debug|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x86.ActiveCfg = Debug|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x86.Build.0 = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.Build.0 = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.ActiveCfg = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.Build.0 = Release|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x86.ActiveCfg = Release|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x86.Build.0 = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x64.ActiveCfg = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x64.Build.0 = Debug|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x86.Build.0 = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|Any CPU.Build.0 = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x64.ActiveCfg = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x64.Build.0 = Release|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x86.ActiveCfg = Release|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x86.Build.0 = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x64.ActiveCfg = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x64.Build.0 = Debug|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x86.Build.0 = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|Any CPU.Build.0 = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.ActiveCfg = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.Build.0 = Release|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x86.ActiveCfg = Release|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x86.Build.0 = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.Build.0 = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.ActiveCfg = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.Build.0 = Debug|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x86.ActiveCfg = Debug|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x86.Build.0 = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.ActiveCfg = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.Build.0 = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.ActiveCfg = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.Build.0 = Release|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x86.ActiveCfg = Release|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x86.Build.0 = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.ActiveCfg = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.Build.0 = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x86.Build.0 = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.Build.0 = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.ActiveCfg = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.Build.0 = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x86.ActiveCfg = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x86.Build.0 = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.ActiveCfg = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.Build.0 = Debug|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x86.Build.0 = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.Build.0 = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.ActiveCfg = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.Build.0 = Release|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x86.ActiveCfg = Release|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x86.Build.0 = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.Build.0 = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.ActiveCfg = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.Build.0 = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x86.Build.0 = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.ActiveCfg = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.Build.0 = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.ActiveCfg = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.Build.0 = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x86.ActiveCfg = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x86.Build.0 = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x64.ActiveCfg = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x64.Build.0 = Debug|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x86.Build.0 = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|Any CPU.ActiveCfg = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|Any CPU.Build.0 = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x64.ActiveCfg = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x64.Build.0 = Release|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x86.ActiveCfg = Release|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x86.Build.0 = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x64.ActiveCfg = Debug|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x64.Build.0 = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x86.Build.0 = Debug|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.Build.0 = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.ActiveCfg = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.Build.0 = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.ActiveCfg = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.Build.0 = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.ActiveCfg = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.Build.0 = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x86.Build.0 = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.Build.0 = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.ActiveCfg = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.Build.0 = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x86.ActiveCfg = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x86.Build.0 = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.ActiveCfg = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.Build.0 = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x86.Build.0 = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.Build.0 = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.ActiveCfg = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.Build.0 = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x86.ActiveCfg = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x86.Build.0 = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x64.ActiveCfg = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x64.Build.0 = Debug|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x86.Build.0 = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|Any CPU.Build.0 = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x64.ActiveCfg = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x64.Build.0 = Release|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x86.ActiveCfg = Release|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x86.Build.0 = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|Any CPU.Build.0 = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x64.ActiveCfg = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x64.Build.0 = Debug|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x86.ActiveCfg = Debug|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x86.Build.0 = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|Any CPU.ActiveCfg = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|Any CPU.Build.0 = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x64.ActiveCfg = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x64.Build.0 = Release|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x86.ActiveCfg = Release|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x86.Build.0 = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x64.ActiveCfg = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x64.Build.0 = Debug|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x86.ActiveCfg = Debug|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x86.Build.0 = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|Any CPU.Build.0 = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x64.ActiveCfg = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x64.Build.0 = Release|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x86.ActiveCfg = Release|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x86.Build.0 = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x64.ActiveCfg = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x64.Build.0 = Debug|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x86.Build.0 = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|Any CPU.Build.0 = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.ActiveCfg = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.Build.0 = Release|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x86.ActiveCfg = Release|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x86.Build.0 = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.ActiveCfg = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.Build.0 = Debug|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x86.Build.0 = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.Build.0 = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.ActiveCfg = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.Build.0 = Release|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x86.ActiveCfg = Release|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x86.Build.0 = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x64.ActiveCfg = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x64.Build.0 = Debug|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x86.Build.0 = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|Any CPU.Build.0 = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.ActiveCfg = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.Build.0 = Release|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x86.ActiveCfg = Release|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x86.Build.0 = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.Build.0 = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.ActiveCfg = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.Build.0 = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x86.ActiveCfg = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x86.Build.0 = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.ActiveCfg = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.Build.0 = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.ActiveCfg = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.Build.0 = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x86.ActiveCfg = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x86.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x64.ActiveCfg = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x64.Build.0 = Debug|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x86.Build.0 = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|x64.ActiveCfg = Debug|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|x64.Build.0 = Debug|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|Any CPU.Build.0 = Release|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|x64.ActiveCfg = Release|Any CPU - {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|x64.Build.0 = Release|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x86.ActiveCfg = Release|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x86.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x86.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x86.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x86.Build.0 = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.ActiveCfg = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x86.Build.0 = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x86.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x86.Build.0 = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|Any CPU.Build.0 = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x64.ActiveCfg = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x64.Build.0 = Debug|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x86.ActiveCfg = Debug|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x86.Build.0 = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.ActiveCfg = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.Build.0 = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.ActiveCfg = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.Build.0 = Release|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x86.ActiveCfg = Release|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x86.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x86.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x86.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x86.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x86.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x64.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x64.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x86.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x86.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|Any CPU.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x64.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x64.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x86.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x86.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x64.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x86.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|Any CPU.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x64.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x64.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x86.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -709,9 +995,13 @@ Global {B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} - {242F2D93-FCCE-4982-8075-F3052ECCA92C} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} + {C548FDFF-B882-B552-D428-5C8EC4478187} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72} = {416C0757-85C6-430B-B973-B72FCDC7EB94} + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} + {416C0757-85C6-430B-B973-B72FCDC7EB94} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index aae7921bf..8ef34f23b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,193 +1,200 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/test-skill/SKILL.md b/skills/test-skill/SKILL.md new file mode 100644 index 000000000..948dca60b --- /dev/null +++ b/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill to verify the Agent Skills framework. +--- + +### Test Skill Instructions +This is a test skill. You can use it to verify if the loading mechanism works. +You can run the `hello.py` script to print a greeting. diff --git a/skills/test-skill/scripts/hello.py b/skills/test-skill/scripts/hello.py new file mode 100644 index 000000000..73f0bea64 --- /dev/null +++ b/skills/test-skill/scripts/hello.py @@ -0,0 +1,4 @@ +import sys + +name = sys.argv[1] if len(sys.argv) > 1 else "World" +print(f"Hello, {name} from Agent Skills!") diff --git a/src/BotSharp.AppHost/Program.cs b/src/BotSharp.AppHost/Program.cs index 4c54ed11b..444e2ecf3 100644 --- a/src/BotSharp.AppHost/Program.cs +++ b/src/BotSharp.AppHost/Program.cs @@ -2,8 +2,8 @@ var apiService = builder.AddProject("apiservice") .WithExternalHttpEndpoints(); -var mcpService = builder.AddProject("mcpservice") - .WithExternalHttpEndpoints(); +//var mcpService = builder.AddProject("mcpservice") +// .WithExternalHttpEndpoints(); builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI") .WithReference(apiService) diff --git a/src/BotSharp.AppHost/Properties/launchSettings.json b/src/BotSharp.AppHost/Properties/launchSettings.json index c315179c6..e4b685d27 100644 --- a/src/BotSharp.AppHost/Properties/launchSettings.json +++ b/src/BotSharp.AppHost/Properties/launchSettings.json @@ -8,6 +8,7 @@ "applicationUrl": "https://localhost:17248;http://localhost:15140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21247", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22140" @@ -20,6 +21,7 @@ "applicationUrl": "http://localhost:15140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19185", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20069" diff --git a/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj b/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj index 78ccdc056..f64f850b0 100644 --- a/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj +++ b/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -21,6 +21,7 @@ + diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index bfc0bb687..b4790f3e7 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -1,12 +1,12 @@ +using Langfuse.OpenTelemetry; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; @@ -45,6 +45,10 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { + // Enable model diagnostics with sensitive data. + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnostics", true); + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true); + builder.Logging.AddOpenTelemetry(logging => { // Use Serilog Log.Logger = new LoggerConfiguration() @@ -87,10 +91,29 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati }) .WithTracing(tracing => { - tracing.AddAspNetCoreInstrumentation() + tracing.SetResourceBuilder( + ResourceBuilder.CreateDefault() + .AddService("apiservice", serviceVersion: "1.0.0") + ) + .AddSource("BotSharp") + .AddSource("BotSharp.Server") + .AddSource("BotSharp.Abstraction.Diagnostics") + .AddSource("BotSharp.Core.Routing.Executor") + .AddLangfuseExporter(builder.Configuration) + .AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); + .AddHttpClientInstrumentation() + //.AddOtlpExporter(options => + //{ + // //options.Endpoint = new Uri(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"); + // options.Endpoint = new Uri(host); + // options.Protocol = OtlpExportProtocol.HttpProtobuf; + // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + //}) + ; + + }); builder.AddOpenTelemetryExporters(); @@ -100,6 +123,8 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) { + var langfuseSection = builder.Configuration.GetSection("Langfuse"); + var useLangfuse = langfuseSection != null; var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) @@ -107,15 +132,9 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli builder.Services.Configure(logging => logging.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - + } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} + return builder; } diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 37f1f2520..761d60372 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -33,6 +33,11 @@ + + + + + + - diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs new file mode 100644 index 000000000..11c44a987 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using BotSharp.Abstraction.Diagnostics.Telemetry; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. +/// This class contains experimental features and may change in the future. +/// To enable these features, set one of the following switches to true: +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` +/// Or set the following environment variables to true: +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` +/// +[ExcludeFromCodeCoverage] +public static class ActivityExtensions +{ + private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; + private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; + private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; + private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; + + public static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); + public static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); + + + /// + /// Starts an activity with the appropriate tags for a kernel function execution. + /// + public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription) + { + const string OperationName = "execute_tool"; + + return source.StartActivityWithTags($"{OperationName} {functionName}", [ + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolName, functionName), + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolDescription, functionDescription) + ], ActivityKind.Internal); + } + + /// + /// Starts an activity with the specified name and tags. + /// + public static Activity? StartActivityWithTags(this ActivitySource source, string name, IEnumerable> tags, ActivityKind kind = ActivityKind.Internal) + => source.StartActivity(name, kind, default(ActivityContext), tags); + + /// + /// Adds tags to the activity. + /// + public static Activity SetTags(this Activity activity, ReadOnlySpan> tags) + { + foreach (var tag in tags) + { + activity.SetTag(tag.Key, tag.Value); + } + return activity; + } + + /// + /// Adds an event to the activity. Should only be used for events that contain sensitive data. + /// + public static Activity AttachSensitiveDataAsEvent(this Activity activity, string name, IEnumerable> tags) + { + activity.AddEvent(new ActivityEvent( + name, + tags: [.. tags] + )); + + return activity; + } + + /// + /// Sets the error status and type on the activity. + /// + public static Activity SetError(this Activity activity, Exception exception) + { + activity.SetTag("error.type", exception.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + return activity; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs new file mode 100644 index 000000000..2add23728 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Helper class to get app context switch value +/// +[ExcludeFromCodeCoverage] +internal static class AppContextSwitchHelper +{ + /// + /// Returns the value of the specified app switch or environment variable if it is set. + /// If the switch or environment variable is not set, return false. + /// The app switch value takes precedence over the environment variable. + /// + /// The name of the app switch. + /// The name of the environment variable. + /// The value of the app switch or environment variable if it is set; otherwise, false. + public static bool GetConfigValue(string appContextSwitchName, string envVarName) + { + if (AppContext.TryGetSwitch(appContextSwitchName, out bool value)) + { + return value; + } + + string? envVarValue = Environment.GetEnvironmentVariable(envVarName); + if (envVarValue != null && bool.TryParse(envVarValue, out value)) + { + return value; + } + + return false; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs new file mode 100644 index 000000000..72c1a03d2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Abstraction.Diagnostics; + +public class BotSharpOTelOptions +{ + public const string DefaultName = "BotSharp.Server"; + + public string Name { get; set; } = DefaultName; + + public string Version { get; set; } = "4.0.0"; + + public bool IsTelemetryEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs new file mode 100644 index 000000000..39fba4d40 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Abstraction.Diagnostics; + +internal static class EnvironmentConfigLoader +{ + private const string DefaultBaseUrl = "https://cloud.langfuse.com"; + + private const string EnvTelemetry = "BOTSHARP_COLLECT_TELEMETRY"; + + + /// + /// Loads configuration from environment variables and applies defaults. + /// + public static BotSharpOTelOptions LoadFromEnvironment(IConfiguration? configuration = null) + { + var options = new BotSharpOTelOptions(); + + // Try configuration first (appsettings.json, etc.) + if (configuration != null) + { + if (bool.TryParse(configuration["Otel:IsTelemetryEnabled"], out bool istelemetryEnabled)) + { + options.IsTelemetryEnabled = istelemetryEnabled; + } + } + + var collectTelemetry = Environment.GetEnvironmentVariable(EnvTelemetry); + if (!string.IsNullOrWhiteSpace(collectTelemetry)) + { + options.IsTelemetryEnabled = bool.TryParse(collectTelemetry, out var shouldCollect) && shouldCollect; + } + + + return options; + } + + /// + /// Validates that required options are set. + /// + public static void Validate(BotSharpOTelOptions options) + { + if (string.IsNullOrWhiteSpace(options.Name)) + { + throw new InvalidOperationException( + $"Otel name is required. Set it via code or configuration."); + } + + } + +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs new file mode 100644 index 000000000..390c0e1ae --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs @@ -0,0 +1,55 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace BotSharp.Abstraction.Diagnostics; + +public static class OpenTelemetryExtensions +{ + public static void AddOpenTelemetry(this IServiceCollection services, + IConfiguration configure) + { + // Load from environment first + var options = EnvironmentConfigLoader.LoadFromEnvironment(configure); + + // Validate configuration + EnvironmentConfigLoader.Validate(options); + + services.Configure(cfg => + { + cfg.Name = options.Name; + cfg.Version = _assemblyVersion.Value; + cfg.IsTelemetryEnabled = options.IsTelemetryEnabled; + }); + + services.AddSingleton(); + services.AddSingleton(); + + } + + /// + /// Align with --version command. + /// https://github.com/dotnet/command-line-api/blob/bcdd4b9b424f0ff6ec855d08665569061a5d741f/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs#L23-L39 + /// + private static readonly Lazy _assemblyVersion = new(() => + { + var assembly = Assembly.GetEntryAssembly(); + + if (assembly == null) + { + throw new InvalidOperationException("Should be able to get entry assembly."); + } + + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + + if (assemblyVersionAttribute is null) + { + return assembly.GetName().Version?.ToString() ?? ""; + } + else + { + return assemblyVersionAttribute.InformationalVersion; + } + }); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs new file mode 100644 index 000000000..74ee63621 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs @@ -0,0 +1,15 @@ +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public interface IMachineInformationProvider +{ + /// + /// Gets existing or creates the device id. In case the cached id cannot be retrieved, or the + /// newly generated id cannot be cached, a value of null is returned. + /// + Task GetOrCreateDeviceId(); + + /// + /// Gets a hash of the machine's MAC address. + /// + Task GetMacAddressHash(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs new file mode 100644 index 000000000..89e3f5966 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs @@ -0,0 +1,66 @@ +using BotSharp.Abstraction.Conversations; +using ModelContextProtocol.Protocol; +using System.Diagnostics; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public interface ITelemetryService : IDisposable +{ + ActivitySource Parent { get; } + + /// + /// Creates and starts a new telemetry activity. + /// + /// Name of the activity. + /// An Activity object or null if there are no active listeners or telemetry is disabled. + /// If the service is not in an operational state or was not invoked. + Activity? StartActivity(string activityName); + + /// + /// Creates and starts a new telemetry activity. + /// + /// Name of the activity. + /// MCP client information to add to the activity. + /// An Activity object or null if there are no active listeners or telemetry is disabled. + /// If the service is not in an operational state or was not invoked. + Activity? StartActivity(string activityName, Implementation? clientInfo); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartTextCompletionActivity(Uri? endpoint, string modelName, string modelProvider, string prompt, IConversationStateService services); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartCompletionActivity(Uri? endpoint, string modelName, string modelProvider, List chatHistory, IConversationStateService conversationStateService); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartAgentInvocationActivity(string agentId, string agentName, string? agentDescription, Agent? agents, List messages); + + /// + /// Performs any initialization operations before telemetry service is ready. + /// + /// A task that completes when initialization is complete. + Task InitializeAsync(); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs new file mode 100644 index 000000000..9cd954f56 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs @@ -0,0 +1,83 @@ +using DeviceId; +using Microsoft.Extensions.Logging; +using System.Net.NetworkInformation; +using System.Security.Cryptography; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public class MachineInformationProvider(ILogger logger) + : IMachineInformationProvider +{ + protected const string NotAvailable = "N/A"; + + private static readonly SHA256 s_sHA256 = SHA256.Create(); + + private readonly ILogger _logger = logger; + + /// + /// + /// + public async Task GetOrCreateDeviceId() + { + string deviceId = new DeviceIdBuilder() + .AddMachineName() + .AddOsVersion() + .OnWindows(windows => windows + .AddProcessorId() + .AddMotherboardSerialNumber() + .AddSystemDriveSerialNumber()) + .OnLinux(linux => linux + .AddMotherboardSerialNumber() + .AddSystemDriveSerialNumber()) + .OnMac(mac => mac + .AddSystemDriveSerialNumber() + .AddPlatformSerialNumber()) + .ToString(); + + return deviceId; + } + + /// + /// + /// + public virtual Task GetMacAddressHash() + { + return Task.Run(() => + { + try + { + var address = GetMacAddress(); + + return address != null + ? HashValue(address) + : NotAvailable; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to calculate MAC address hash."); + return NotAvailable; + } + }); + } + + /// + /// Searches for first network interface card that is up and has a physical address. + /// + /// Hash of the MAC address or if none can be found. + protected virtual string? GetMacAddress() + { + return NetworkInterface.GetAllNetworkInterfaces() + .Where(x => x.OperationalStatus == OperationalStatus.Up && x.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .Select(x => x.GetPhysicalAddress().ToString()) + .FirstOrDefault(x => !string.IsNullOrEmpty(x)); + } + + /// + /// Generates a SHA-256 of the given value. + /// + protected string HashValue(string value) + { + var hashInput = s_sHA256.ComputeHash(Encoding.UTF8.GetBytes(value)); + return BitConverter.ToString(hashInput).Replace("-", string.Empty).ToLowerInvariant(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs new file mode 100644 index 000000000..88433d8ce --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs @@ -0,0 +1,98 @@ +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public static class TelemetryConstants +{ + /// + /// Name of tags published. + /// + public static class TagName + { + public const string BotSharpVersion = "Version"; + public const string ClientName = "ClientName"; + public const string ClientVersion = "ClientVersion"; + public const string DevDeviceId = "DevDeviceId"; + public const string ErrorDetails = "ErrorDetails"; + public const string EventId = "EventId"; + public const string MacAddressHash = "MacAddressHash"; + public const string ToolName = "ToolName"; + public const string ToolArea = "ToolArea"; + public const string ServerMode = "ServerMode"; + public const string IsServerCommandInvoked = "IsServerCommandInvoked"; + public const string Transport = "Transport"; + public const string IsReadOnly = "IsReadOnly"; + public const string Namespace = "Namespace"; + public const string ToolCount = "ToolCount"; + public const string InsecureDisableElicitation = "InsecureDisableElicitation"; + public const string IsDebug = "IsDebug"; + public const string EnableInsecureTransports = "EnableInsecureTransports"; + public const string Tool = "Tool"; + } + + public static class ActivityName + { + public const string ListToolsHandler = "ListToolsHandler"; + public const string ToolExecuted = "ToolExecuted"; + public const string ServerStarted = "ServerStarted"; + } + + /// + /// 工具输入输出参数键常量类 + /// + public static class ToolParameterKeys + { + /// + /// 输入参数键 + /// + public const string Input = "input"; + + /// + /// 输出参数键 + /// + public const string Output = "output"; + } + + /// + /// Tags used in model diagnostics + /// + public static class ModelDiagnosticsTags + { + // Activity tags + public const string System = "gen_ai.system"; + public const string Operation = "gen_ai.operation.name"; + public const string Model = "gen_ai.request.model"; + public const string MaxToken = "gen_ai.request.max_tokens"; + public const string Temperature = "gen_ai.request.temperature"; + public const string TopP = "gen_ai.request.top_p"; + public const string ResponseId = "gen_ai.response.id"; + public const string ResponseModel = "gen_ai.response.model"; + public const string FinishReason = "gen_ai.response.finish_reason"; + public const string InputTokens = "gen_ai.usage.input_tokens"; + public const string OutputTokens = "gen_ai.usage.output_tokens"; + public const string Address = "server.address"; + public const string Port = "server.port"; + public const string AgentId = "gen_ai.agent.id"; + public const string AgentName = "gen_ai.agent.name"; + public const string AgentDescription = "gen_ai.agent.description"; + public const string AgentInvocationInput = "gen_ai.input.messages"; + public const string AgentInvocationOutput = "gen_ai.output.messages"; + public const string AgentToolDefinitions = "gen_ai.tool.definitions"; + + // Activity events + public const string EventName = "gen_ai.event.content"; + public const string SystemMessage = "gen_ai.system.message"; + public const string UserMessage = "gen_ai.user.message"; + public const string AssistantMessage = "gen_ai.assistant.message"; + public const string ToolName = "gen_ai.tool.name"; + public const string ToolMessage = "gen_ai.tool.message"; + public const string ToolDescription = "gen_ai.tool.description"; + public const string Choice = "gen_ai.choice"; + + public static readonly Dictionary RoleToEventMap = new() + { + { AgentRole.System, SystemMessage }, + { AgentRole.User, UserMessage }, + { AgentRole.Assistant, AssistantMessage }, + { AgentRole.Function, ToolMessage } + }; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs new file mode 100644 index 000000000..f1414daef --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs @@ -0,0 +1,378 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public class TelemetryService : ITelemetryService +{ + private readonly IMachineInformationProvider _informationProvider; + private readonly bool _isEnabled; + private readonly ILogger _logger; + private readonly List> _tagsList; + private readonly SemaphoreSlim _initalizeLock = new(1); + + /// + /// Task created on the first invocation of . + /// This is saved so that repeated invocations will see the same exception + /// as the first invocation. + /// + private Task? _initalizationTask = null; + + private bool _initializationSuccessful; + private bool _isInitialized; + + public ActivitySource Parent { get; } + + public TelemetryService(IMachineInformationProvider informationProvider, + IOptions options, + ILogger logger) + { + _isEnabled = options.Value.IsTelemetryEnabled; + _tagsList = + [ + new(TelemetryConstants.TagName.BotSharpVersion, options.Value.Version), + ]; + + + Parent = new ActivitySource(options.Value.Name, options.Value.Version); + _informationProvider = informationProvider; + _logger = logger; + } + + /// + /// TESTING PURPOSES ONLY: Gets the default tags used for telemetry. + /// + internal IReadOnlyList> GetDefaultTags() + { + if (!_isEnabled) + { + return []; + } + + CheckInitialization(); + return [.. _tagsList]; + } + + /// + /// + /// + public Activity? StartActivity(string activityId) => StartActivity(activityId, null); + + /// + /// + /// + public Activity? StartActivity(string activityId, Implementation? clientInfo) + { + if (!_isEnabled) + { + return null; + } + + CheckInitialization(); + + var activity = Parent.StartActivity(activityId); + + if (activity == null) + { + return activity; + } + + if (clientInfo != null) + { + activity.AddTag(TelemetryConstants.TagName.ClientName, clientInfo.Name) + .AddTag(TelemetryConstants.TagName.ClientVersion, clientInfo.Version); + } + + activity.AddTag(TelemetryConstants.TagName.EventId, Guid.NewGuid().ToString()); + + _tagsList.ForEach(kvp => activity.AddTag(kvp.Key, kvp.Value)); + + return activity; + } + + public Activity? StartTextCompletionActivity(Uri? endpoint, string modelName, string modelProvider, string prompt, IConversationStateService services) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "text.completions"; + var activity = Parent.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + new(TelemetryConstants.ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(TelemetryConstants.ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(TelemetryConstants.ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, services); + + if (ActivityExtensions.s_enableSensitiveEvents) + { + activity?.AttachSensitiveDataAsEvent( + TelemetryConstants.ModelDiagnosticsTags.UserMessage, + [ + new(TelemetryConstants.ModelDiagnosticsTags.EventName, prompt), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + ]); + } + + return activity; + } + + public Activity? StartCompletionActivity(Uri? endpoint, string modelName, string modelProvider, List chatHistory, IConversationStateService conversationStateService) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "chat.completions"; + var activity = Parent.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + new(TelemetryConstants.ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(TelemetryConstants.ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(TelemetryConstants.ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, conversationStateService); + + if (ActivityExtensions.s_enableSensitiveEvents) + { + foreach (var message in chatHistory) + { + var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); + activity?.AttachSensitiveDataAsEvent( + TelemetryConstants.ModelDiagnosticsTags.RoleToEventMap[message.Role], + [ + new(TelemetryConstants.ModelDiagnosticsTags.EventName, formattedContent), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + ]); + } + } + + return activity; + } + + public Activity? StartAgentInvocationActivity(string agentId, string agentName, string? agentDescription, Agent? agents, List messages) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "invoke_agent"; + + var activity = Parent.StartActivityWithTags( + $"{OperationName} {agentName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.AgentId, agentId), + new(TelemetryConstants.ModelDiagnosticsTags.AgentName, agentName) + ], + ActivityKind.Internal); + + if (!string.IsNullOrWhiteSpace(agentDescription)) + { + activity?.SetTag(TelemetryConstants.ModelDiagnosticsTags.AgentDescription, agentDescription); + } + + if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count > 0)) + { + List allFunctions = []; + allFunctions.AddRange(agents.Functions); + allFunctions.AddRange(agents.SecondaryFunctions); + + activity?.SetTag( + TelemetryConstants.ModelDiagnosticsTags.AgentToolDefinitions, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + if (IsSensitiveEventsEnabled()) + { + activity?.SetTag( + TelemetryConstants.ModelDiagnosticsTags.AgentInvocationInput, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + return activity; + } + + + public void Dispose() + { + + } + + /// + /// + /// + public async Task InitializeAsync() + { + if (!_isEnabled) + { + return; + } + + // Quick check if initialization already happened. Avoids + // trying to get the lock. + if (_initalizationTask == null) + { + // Get async lock for starting initialization + await _initalizeLock.WaitAsync(); + + try + { + // Check after acquiring lock to ensure we honor work + // started while we were waiting. + if (_initalizationTask == null) + { + _initalizationTask = InnerInitializeAsync(); + } + } + finally + { + _initalizeLock.Release(); + } + } + + // Await the response of the initialization work regardless of if + // we or another invocation created the Task representing it. All + // awaiting on this will give the same result to ensure idempotency. + await _initalizationTask; + + async Task InnerInitializeAsync() + { + try + { + var macAddressHash = await _informationProvider.GetMacAddressHash(); + var deviceId = await _informationProvider.GetOrCreateDeviceId(); + + _tagsList.Add(new(TelemetryConstants.TagName.MacAddressHash, macAddressHash)); + _tagsList.Add(new(TelemetryConstants.TagName.DevDeviceId, deviceId)); + + _initializationSuccessful = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred initializing telemetry service."); + throw; + } + finally + { + _isInitialized = true; + } + } + } + + private void CheckInitialization() + { + if (!_isInitialized) + { + throw new InvalidOperationException( + $"Telemetry service has not been initialized. Use {nameof(InitializeAsync)}() before any other operations."); + } + + if (!_initializationSuccessful) + { + throw new InvalidOperationException("Telemetry service was not successfully initialized. Check logs for initialization errors."); + } + + } + + internal bool IsModelDiagnosticsEnabled() + { + return (ActivityExtensions.s_enableDiagnostics || ActivityExtensions.s_enableSensitiveEvents) && Parent.HasListeners(); + } + + /// + /// Check if sensitive events are enabled. + /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. + /// + internal bool IsSensitiveEventsEnabled() => ActivityExtensions.s_enableSensitiveEvents && Parent.HasListeners(); + + private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) + { + if (activity is null) + { + return; + } + + void TryAddTag(string key, string tag) + { + var value = conversationStateService.GetState(key); + if (!string.IsNullOrEmpty(value)) + { + activity.SetTag(tag, value); + } + } + + TryAddTag("max_tokens", TelemetryConstants.ModelDiagnosticsTags.MaxToken); + TryAddTag("temperature", TelemetryConstants.ModelDiagnosticsTags.Temperature); + TryAddTag("top_p", TelemetryConstants.ModelDiagnosticsTags.TopP); + } + + /// + /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) + { + return new + { + role = chatMessage.Role.ToString(), + name = chatMessage.MessageId, + content = chatMessage.Content, + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), + }; + } + + /// + /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format + /// + private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) + { + List toolCalls = []; + if (chatMessage.Instruction is not null) + { + toolCalls.Add(new + { + id = chatMessage.ToolCallId, + function = new + { + name = chatMessage.Instruction.Function, + arguments = chatMessage.Instruction.Arguments + }, + type = "function" + }); + } + return toolCalls; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 9c34de599..eaa76bef3 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -16,6 +16,7 @@ using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Realtime; using BotSharp.Abstraction.Repositories.Settings; +using BotSharp.Abstraction.Diagnostics; namespace BotSharp.Core; @@ -31,7 +32,7 @@ public static IServiceCollection AddBotSharpCore(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddOpenTelemetry(config); AddRedisEvents(services, config); // Register cache service AddCacheServices(services, config); diff --git a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs index 9a06a85b8..c0a46c15d 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs @@ -30,13 +30,15 @@ public McpClientManager( IClientTransport? transport = null; if (config.SseConfig != null) { - transport = new HttpClientTransport(new HttpClientTransportOptions - { - Name = config.Name, - Endpoint = new Uri(config.SseConfig.EndPoint), - AdditionalHeaders = config.SseConfig.AdditionalHeaders, - ConnectionTimeout = config.SseConfig.ConnectionTimeout - }); + transport = new HttpClientTransport( + new HttpClientTransportOptions + { + Endpoint = new Uri(config.SseConfig.EndPoint), + TransportMode = HttpTransportMode.AutoDetect, + Name = config.Name, + ConnectionTimeout = config.SseConfig.ConnectionTimeout, + AdditionalHeaders = config.SseConfig.AdditionalHeaders + }); } else if (config.StdioConfig != null) { @@ -62,7 +64,7 @@ public McpClientManager( _logger.LogWarning(ex, $"Error when loading mcp client {serverId}"); return null; } - } + } public void Dispose() { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 4b208374f..63490d12f 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -1,20 +1,31 @@ -using BotSharp.Abstraction.Routing.Executor; +using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Routing.Executor; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Core.Routing.Executor; public class FunctionCallbackExecutor : IFunctionExecutor { private readonly IFunctionCallback _functionCallback; + private readonly ITelemetryService _telemetryService; - public FunctionCallbackExecutor(IFunctionCallback functionCallback) + public FunctionCallbackExecutor(ITelemetryService telemetryService, IFunctionCallback functionCallback) { _functionCallback = functionCallback; + _telemetryService = telemetryService; } public async Task ExecuteAsync(RoleDialogModel message) { - return await _functionCallback.Execute(message); + using var activity = _telemetryService.Parent.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); + { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); + return await _functionCallback.Execute(message); + } } public async Task GetIndicatorAsync(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs index 8a4a54865..516317c2f 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Executor; @@ -7,10 +8,12 @@ internal class FunctionExecutorFactory { public static IFunctionExecutor? Create(IServiceProvider services, string functionName, Agent agent) { + ITelemetryService telemetryService = services.GetRequiredService(); + var functionCall = services.GetServices().FirstOrDefault(x => x.Name == functionName); if (functionCall != null) { - return new FunctionCallbackExecutor(functionCall); + return new FunctionCallbackExecutor( telemetryService,functionCall); } var functions = (agent?.Functions ?? []).Concat(agent?.SecondaryFunctions ?? []); @@ -23,7 +26,7 @@ internal class FunctionExecutorFactory var mcpServerId = agent?.McpTools?.Where(x => x.Functions.Any(y => y.Name == funcDef?.Name))?.FirstOrDefault()?.ServerId; if (!string.IsNullOrWhiteSpace(mcpServerId)) { - return new McpToolExecutor(services, mcpServerId, functionName); + return new McpToolExecutor(services, telemetryService, mcpServerId, functionName); } return null; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index 8cf7d18e5..36e7d70df 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,6 +1,11 @@ +using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; +using ModelContextProtocol; using ModelContextProtocol.Protocol; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Core.Routing.Executor; @@ -9,44 +14,51 @@ public class McpToolExecutor : IFunctionExecutor private readonly IServiceProvider _services; private readonly string _mcpServerId; private readonly string _functionName; + private readonly ITelemetryService _telemetryService; + - public McpToolExecutor(IServiceProvider services, string mcpServerId, string functionName) - { + public McpToolExecutor(IServiceProvider services, ITelemetryService telemetryService, string mcpServerId, string functionName) + { _services = services; + _telemetryService = telemetryService; _mcpServerId = mcpServerId; _functionName = functionName; } public async Task ExecuteAsync(RoleDialogModel message) { - try + using var activity = _telemetryService.Parent.StartFunctionActivity(this._functionName, $"calling tool {_functionName} of MCP server {_mcpServerId}"); { - // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = JsonToDictionary(message.FunctionArgs); - - var clientManager = _services.GetRequiredService(); - var client = await clientManager.GetMcpClientAsync(_mcpServerId); - - if (client == null) + try + { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); + + // Convert arguments to dictionary format expected by mcpdotnet + Dictionary argDict = JsonToDictionary(message.FunctionArgs); + + var clientManager = _services.GetRequiredService(); + var client = await clientManager.GetMcpClientAsync(_mcpServerId); + + // Call the tool through mcpdotnet + var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); + + // Extract the text content from the result + var json = string.Join("\n", result.Content + .OfType() + .Where(c => c.Type == "text") + .Select(c => c.Text)); + + message.Content = json; + message.Data = json.JsonContent(); + return true; + } + catch (Exception ex) { - message.Content = $"MCP client for server {_mcpServerId} not found."; + message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; + activity?.SetError(ex); return false; } - - // Call the tool through mcpdotnet - var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); - - // Extract the text content from the result - var json = string.Join("\n", result.Content.Where(c => c is TextContentBlock).Select(c => ((TextContentBlock)c).Text)); - - message.Content = json; - message.Data = json.JsonContent(); - return true; - } - catch (Exception ex) - { - message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; - return false; } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index e0175a70d..6132ee534 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; @@ -14,6 +15,8 @@ public async Task InvokeAgent( var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); + using var activity = _telemetryService.StartAgentInvocationActivity(agentId, agent.Name, agent.Description, agent, dialogs); + Context.IncreaseRecursiveCounter(); if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) { @@ -79,7 +82,7 @@ public async Task InvokeAgent( dialogs.Add(message); Context.AddDialogs([message]); } - + return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 3850dcc13..17cd180a3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Core.MessageHub; using BotSharp.Core.Routing.Executor; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4e43cbd52..b327daff1 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,5 +1,7 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; +using System.Diagnostics; namespace BotSharp.Core.Routing; @@ -8,6 +10,7 @@ public partial class RoutingService : IRoutingService private readonly IServiceProvider _services; private readonly RoutingSettings _settings; private readonly IRoutingContext _context; + private readonly ITelemetryService _telemetryService; private readonly ILogger _logger; private Agent _router; @@ -18,11 +21,13 @@ public RoutingService( IServiceProvider services, RoutingSettings settings, IRoutingContext context, + ITelemetryService telemetryService, ILogger logger) { _services = services; _settings = settings; _context = context; + _telemetryService = telemetryService; _logger = logger; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 2a63ae3f7..cc3699891 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -1,17 +1,20 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core.Users.Services; +using BotSharp.OpenAPI.BackgroundServices; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; -using Microsoft.IdentityModel.JsonWebTokens; using BotSharp.OpenAPI.BackgroundServices; +using Microsoft.OpenApi.Models; using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Authentication; namespace BotSharp.OpenAPI; @@ -217,6 +220,7 @@ public static IApplicationBuilder UseBotSharpOpenAPI(this IApplicationBuilder ap app.UseAuthorization(); + app.UseOtelInitialize(); app.UseEndpoints( endpoints => { @@ -260,5 +264,17 @@ public static IApplicationBuilder UseBotSharpUI(this IApplicationBuilder app, bo return app; } + + internal static void UseOtelInitialize(this IApplicationBuilder app) + { + // Perform any initialization before starting the service. + // If the initialization operation fails, do not continue because we do not want + // invalid telemetry published. + var telemetryService = app.ApplicationServices.GetRequiredService(); + telemetryService.InitializeAsync() + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs index 0931466ac..794ec6775 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs @@ -6,6 +6,8 @@ using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.OpenAPI.Controllers; @@ -42,8 +44,12 @@ public async Task NewConversation([FromRoute] string agen }; conv = await service.NewConversation(conv); service.SetConversationId(conv.Id, config.States); - - return ConversationViewModel.FromSession(conv); + using (var trace = new ActivitySource("BotSharp").StartActivity("NewUserSession", ActivityKind.Internal)) + { + trace?.SetTag("user_id", _user.FullName); + trace?.SetTag("conversation_id", conv.Id); + return ConversationViewModel.FromSession(conv); + } } [HttpGet("/conversations")] @@ -363,25 +369,34 @@ public async Task SendMessage( conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, - replyMessage: input.Postback, - async msg => - { - response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; - response.Function = msg.FunctionName; - response.MessageLabel = msg.MessageLabel; - response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; - response.Instruction = msg.Instruction; - response.Data = msg.Data; - }); + using (var trace = new ActivitySource("BotSharp").StartActivity("UserSession", ActivityKind.Internal)) + { + trace?.SetTag("user.id", _user.FullName); + trace?.SetTag("session.id", conversationId); + trace?.SetTag("input", inputMsg.Content); + trace?.SetTag(ModelDiagnosticsTags.AgentId, agentId); + + var response = new ChatResponseModel(); + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); - var state = _services.GetRequiredService(); - response.States = state.GetStates(); - response.MessageId = inputMsg.MessageId; - response.ConversationId = conversationId; + var state = _services.GetRequiredService(); + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; - return response; + trace?.SetTag("output", response.Data); + return response; + } } @@ -432,7 +447,7 @@ await conv.SendMessage(agentId, inputMsg, response.Instruction = msg.Instruction; response.Data = msg.Data; response.States = state.GetStates(); - + await OnChunkReceived(Response, response); }); diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs new file mode 100644 index 000000000..1eda6cebe --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs @@ -0,0 +1,38 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Abstraction.Settings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.AgentSkills.Settings; +using BotSharp.Plugin.AgentSkills.Hooks; +using BotSharp.Plugin.AgentSkills.Functions; +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Plugin.AgentSkills; + +public class AgentSkillsPlugin : IBotSharpPlugin +{ + public string Id => "b6c93605-246e-4f7f-8559-467385501865"; + public string Name => "Agent Skills"; + public string Description => "Enables Anthropic's Agent Skills standard (progressive disclosure of tools)."; + public string IconUrl => "https://avatars.githubusercontent.com/u/108622152?s=200&v=4"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + // 注册设置 + var settings = new AgentSkillsSettings(); + config.Bind("AgentSkills", settings); + services.AddSingleton(settings); + + // 注册核心服务 + services.AddSingleton(); + + // 注册 Hook + services.AddScoped(); + + // 注册 Function Tools + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj b/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj new file mode 100644 index 000000000..155722e79 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + net8.0 + latest + enable + enable + + + diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs new file mode 100644 index 000000000..47c0d3495 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Conversations; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Text.Json; + +namespace BotSharp.Plugin.AgentSkills.Functions; + +public class LoadSkillFn : IFunctionCallback +{ + public string Name => "load_skill"; + public string Indication => "Loading skill..."; + private readonly IServiceProvider _services; + + public LoadSkillFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var skillName = message.FunctionName == "load_skill" + ? JsonSerializer.Deserialize(message.FunctionArgs).GetProperty("skill_name").GetString() + : null; + + if (string.IsNullOrEmpty(skillName)) + { + message.Content = "Error: skill_name provided."; + return false; + } + + var stateService = _services.GetRequiredService(); + var currentActiveStr = stateService.GetState("active_skills"); + var currentActive = string.IsNullOrEmpty(currentActiveStr) + ? new List() + : currentActiveStr.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); + + if (!currentActive.Contains(skillName)) + { + currentActive.Add(skillName); + stateService.SetState("active_skills", string.Join(",", currentActive)); + message.Content = $"Skill '{skillName}' has been activated. The detailed instructions will be available in the next step."; + } + else + { + message.Content = $"Skill '{skillName}' is already active."; + } + + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs new file mode 100644 index 000000000..a78f8427c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs @@ -0,0 +1,77 @@ +using System.Threading.Tasks; +using System.Text.Json; +using System; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.PythonInterpreter.Interfaces; + +namespace BotSharp.Plugin.AgentSkills.Functions; + +public class RunSkillScriptFn : IFunctionCallback +{ + public string Name => "run_skill_script"; + public string Indication => "Running skill script..."; + private readonly IServiceProvider _services; + + public RunSkillScriptFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var skillName = args.TryGetProperty("skill_name", out var t1) ? t1.GetString() : null; + var scriptFile = args.TryGetProperty("script_file", out var t2) ? t2.GetString() : null; + // 简单处理 args,假设直接是字符串形式的命令行参数,或者 JSON 字符串 + // 如果是 JSON 对象,需要转换。为了简单起见,这里假设 LLM 传入的是参数字符串 + var scriptArgs = args.TryGetProperty("args", out var t3) ? t3.GetString() : ""; + + if (string.IsNullOrEmpty(skillName) || string.IsNullOrEmpty(scriptFile)) + { + message.Content = "Error: skill_name and script_file are required."; + return false; + } + + var skillService = _services.GetRequiredService(); + string scriptPath; + try + { + scriptPath = skillService.GetScriptPath(skillName, scriptFile); + if (string.IsNullOrEmpty(scriptPath)) + { + message.Content = $"Error: Script '{scriptFile}' not found in skill '{skillName}'."; + return false; + } + } + catch (Exception ex) + { + message.Content = $"Error: {ex.Message}"; + return false; + } + + // 目前仅支持 .py + if (scriptPath.EndsWith(".py")) + { + var runner = _services.GetRequiredService(); + try + { + var output = await runner.RunScript(scriptPath, scriptArgs); + message.Content = output; + return true; + } + catch (Exception ex) + { + message.Content = $"Script execution error: {ex.Message}"; + return false; + } + } + else + { + message.Content = "Error: Only .py scripts are supported currently."; + return false; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs new file mode 100644 index 000000000..7c4b562fa --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Settings; +using BotSharp.Plugin.AgentSkills.Services; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Abstraction.Conversations; +using System.Text.Json; + +namespace BotSharp.Plugin.AgentSkills.Hooks; + +public class AgentSkillHook : AgentHookBase +{ + public override string SelfId => string.Empty; + + public AgentSkillHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override bool OnInstructionLoaded(string template, IDictionary dict) + { + if(Agent.Type == AgentType.Routing || Agent.Type == AgentType.Planning) + { + return base.OnInstructionLoaded(template, dict); + } + var skillService = _services.GetRequiredService(); + var stateService = _services.GetRequiredService(); + + // 1. Discovery Phase: Inject Available Skills + var availableSkills = skillService.GetAvailableSkills().Result; // Sync for hook + if (availableSkills.Any()) + { + var skillMenu = "\n\n[Available Agent Skills]\nYou have access to the following specialized skills. If a task requires one, call the 'load_skill' function with the skill name.\n"; + foreach (var skill in availableSkills) + { + skillMenu += $"- {skill.Name}: {skill.Description}\n"; + } + + // 将菜单追加到 System Instruction 中 + // 注意:BotSharp 的 OnInstructionLoaded 允许修改 dict 还是 template? + // 假设我们修改 Agent.Instruction 或追加到 Context + this.Agent.Instruction += skillMenu; + } + + // 2. Activation Phase: Inject Active Skills + var activeSkillsJson = stateService.GetState("active_skills"); + if (!string.IsNullOrEmpty(activeSkillsJson)) + { + // 简单的 CSV 解析或 Json 解析,视 load_skill 存储格式而定 + // 假设 active_skills 是逗号分隔的字符串 + var activeSkillNames = activeSkillsJson.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach(var name in activeSkillNames) + { + var skill = skillService.GetSkill(name.Trim()).Result; + if (skill != null) + { + this.Agent.Instruction += $"\n\n### ACTIVE SKILL: {skill.Name.ToUpper()}\n{skill.MarkdownBody}\n"; + } + } + } + + return base.OnInstructionLoaded(template, dict); + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs new file mode 100644 index 000000000..36dcf61d4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace BotSharp.Plugin.AgentSkills.Models; + +public class AgentSkill +{ + public string Name { get; set; } + public string Description { get; set; } + public string MarkdownBody { get; set; } + public string BaseDir { get; set; } + public List Scripts { get; set; } = new List(); + public List Resources { get; set; } = new List(); +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs new file mode 100644 index 000000000..392cc6b94 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs @@ -0,0 +1,12 @@ +using YamlDotNet.Serialization; + +namespace BotSharp.Plugin.AgentSkills.Models; + +public class SkillFrontmatter +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "description")] + public string Description { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs new file mode 100644 index 000000000..e16d257bd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using BotSharp.Plugin.AgentSkills.Models; +using BotSharp.Plugin.AgentSkills.Settings; + +namespace BotSharp.Plugin.AgentSkills.Services; + +public class AgentSkillService : IAgentSkillService, IDisposable +{ + private readonly AgentSkillsSettings _settings; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _skills = new(); + private readonly FileSystemWatcher _watcher; + private readonly IDeserializer _yamlDeserializer; + + public AgentSkillService(AgentSkillsSettings settings, ILogger logger) + { + _settings = settings; + _logger = logger; + _yamlDeserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + // 初始扫描 + RefreshSkills().Wait(); + + // 配置 FileSystemWatcher + var skillDir = Path.IsPathRooted(_settings.DataDir) + ? _settings.DataDir + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _settings.DataDir); + + if (Directory.Exists(skillDir)) + { + _watcher = new FileSystemWatcher(skillDir); + _watcher.Filter = "SKILL.md"; + _watcher.IncludeSubdirectories = true; + _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; + _watcher.Changed += OnSkillChanged; + _watcher.Created += OnSkillChanged; + _watcher.Deleted += OnSkillChanged; + _watcher.EnableRaisingEvents = true; + } + } + + private void OnSkillChanged(object sender, FileSystemEventArgs e) + { + _logger.LogInformation($"Detected change in skills: {e.FullPath}. Refreshing..."); + // 简单暴力:重新扫描。优化点:只更新变动的文件。 + RefreshSkills().Wait(); + } + + public async Task RefreshSkills() + { + var skillDir = Path.IsPathRooted(_settings.DataDir) + ? _settings.DataDir + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _settings.DataDir); + + if (!Directory.Exists(skillDir)) + { + _logger.LogWarning($"Skills directory not found: {skillDir}"); + try + { + Directory.CreateDirectory(skillDir); + } + catch(Exception ex) + { + _logger.LogError(ex, $"Failed to create skills directory at {skillDir}"); + return; + } + } + + var newSkills = new Dictionary(); + var skillFiles = Directory.GetFiles(skillDir, "SKILL.md", SearchOption.AllDirectories); + + foreach (var file in skillFiles) + { + try + { + var skill = ParseSkill(file); + if (skill != null) + { + newSkills[skill.Name] = skill; + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to parse skill at {file}"); + } + } + + _skills.Clear(); + foreach (var kv in newSkills) + { + _skills[kv.Key] = kv.Value; + } + + _logger.LogInformation($"Loaded {_skills.Count} skills."); + } + + private AgentSkill ParseSkill(string filePath) + { + var content = File.ReadAllText(filePath); + + // 简单的 Frontmatter 解析:查找两个 --- 之间的内容 + // 注意:不完美,假设文件严格以 --- 开头 + if (!content.StartsWith("---")) return null; + + var endYaml = content.IndexOf("---", 3); + if (endYaml == -1) return null; + + var yaml = content.Substring(3, endYaml - 3); + var markdown = content.Substring(endYaml + 3).Trim(); + + var frontmatter = _yamlDeserializer.Deserialize(yaml); + if (string.IsNullOrWhiteSpace(frontmatter.Name)) return null; + + var baseDir = Path.GetDirectoryName(filePath); + var scriptDir = Path.Combine(baseDir, "scripts"); + var scripts = Directory.Exists(scriptDir) + ? Directory.GetFiles(scriptDir).Select(Path.GetFileName).ToList() + : new List(); + + var resourceDir = Path.Combine(baseDir, "resources"); + var resources = Directory.Exists(resourceDir) + ? Directory.GetFiles(resourceDir).Select(Path.GetFileName).ToList() + : new List(); + + return new AgentSkill + { + Name = frontmatter.Name, + Description = frontmatter.Description, + MarkdownBody = markdown, + BaseDir = baseDir, + Scripts = scripts, + Resources = resources + }; + } + + public Task> GetAvailableSkills() + { + return Task.FromResult(_skills.Values.ToList()); + } + + public Task GetSkill(string name) + { + if (_skills.TryGetValue(name, out var skill)) + { + return Task.FromResult(skill); + } + return Task.FromResult(null); + } + + public string GetScriptPath(string skillName, string scriptFile) + { + if (_skills.TryGetValue(skillName, out var skill)) + { + // 安全检查:防止路径遍历 + if (scriptFile.Contains("..") || Path.IsPathRooted(scriptFile)) + throw new ArgumentException("Invalid script path"); + + var path = Path.Combine(skill.BaseDir, "scripts", scriptFile); + if (File.Exists(path)) return path; + } + return null; + } + + public void Dispose() + { + if (_watcher != null) + { + _watcher.EnableRaisingEvents = false; + _watcher.Dispose(); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs new file mode 100644 index 000000000..fd5d6e706 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BotSharp.Plugin.AgentSkills.Models; + +namespace BotSharp.Plugin.AgentSkills.Services; + +public interface IAgentSkillService +{ + Task> GetAvailableSkills(); + Task GetSkill(string name); + string GetScriptPath(string skillName, string scriptFile); + Task RefreshSkills(); +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs new file mode 100644 index 000000000..f9dfa1cfb --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Plugin.AgentSkills.Settings; + +public class AgentSkillsSettings +{ + public string DataDir { get; set; } = "skills"; +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index d6b4c4107..571f01232 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -1,11 +1,13 @@ using Anthropic.SDK.Common; using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.AnthropicAI.Providers; @@ -17,22 +19,26 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AnthropicSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; public ChatCompletionProvider(AnthropicSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -45,53 +51,61 @@ public async Task GetChatCompletions(Agent agent, List().FirstOrDefault(); - var toolResult = response.Content.OfType().First(); - responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty) + var response = await client.Messages.GetClaudeMessageAsync(parameters); + + RoleDialogModel responseMessage; + activity?.SetTag(ModelDiagnosticsTags.FinishReason, response.StopReason); + if (response.StopReason == "tool_use") { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolResult.Id, - FunctionName = toolResult.Name, - FunctionArgs = JsonSerializer.Serialize(toolResult.Input), - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - else - { - var message = response.FirstMessage; - responseMessage = new RoleDialogModel(AgentRole.Assistant, message?.Text ?? string.Empty) + var content = response.Content.OfType().FirstOrDefault(); + var toolResult = response.Content.OfType().First(); + + responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolResult.Id, + FunctionName = toolResult.Name, + FunctionArgs = JsonSerializer.Serialize(toolResult.Input), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + } + else { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } + var message = response.FirstMessage; + responseMessage = new RoleDialogModel(AgentRole.Assistant, message?.Text ?? string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + } - var tokenUsage = response.Usage; + var tokenUsage = response.Usage; + var inputTokenDetails = tokenUsage?.InputTokens ?? 0; + var outputTokenDetails = tokenUsage?.OutputTokens ?? 0; + var cachedInputTokens = tokenUsage?.CacheReadInputTokens ; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (inputTokenDetails - cachedInputTokens)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokenDetails); + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = tokenUsage?.InputTokens ?? 0, - TextOutputTokens = tokenUsage?.OutputTokens ?? 0 - }); + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = tokenUsage?.InputTokens ?? 0, + TextOutputTokens = tokenUsage?.OutputTokens ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; } - - return responseMessage; } public Task GetChatCompletionsAsync(Agent agent, List conversations, diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index dc9a0fbc5..8c1c4d7d9 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; @@ -7,6 +8,8 @@ using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; +using BotSharp.Abstraction.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.AzureOpenAI.Providers.Chat; @@ -15,6 +18,7 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AzureOpenAiSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; @@ -25,16 +29,19 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( AzureOpenAiSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -49,91 +56,100 @@ public async Task GetChatCompletions(Agent agent, List? response = null; ChatCompletion value = default; RoleDialogModel responseMessage; - - try + using (var activity = _telemetryService.StartCompletionActivity(null, _model, Provider, conversations, convService)) { - response = chatClient.CompleteChat(messages, options); - value = response.Value; + try + { + response = chatClient.CompleteChat(messages, options); + value = response.Value; - var reason = value.FinishReason; - var content = value.Content; - var text = content.FirstOrDefault()?.Text ?? string.Empty; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; - if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + { + var toolCall = value.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.FunctionName, + FunctionArgs = toolCall?.FunctionArguments?.ToString(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + + // Somethings LLM will generate a function name with agent name. + if (!string.IsNullOrEmpty(responseMessage.FunctionName)) + { + responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); + } + } + else + { + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } + } + catch (ClientResultException ex) { - var toolCall = value.ToolCalls.FirstOrDefault(); - responseMessage = new RoleDialogModel(AgentRole.Function, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolCall?.Id, - FunctionName = toolCall?.FunctionName, - FunctionArgs = toolCall?.FunctionArguments?.ToString(), RenderedInstruction = string.Join("\r\n", renderedInstructions) }; - - // Somethings LLM will generate a function name with agent name. - if (!string.IsNullOrEmpty(responseMessage.FunctionName)) - { - responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); - } + activity?.SetError(ex); } - else + catch (Exception ex) { - responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions), - Annotations = value.Annotations?.Select(x => new ChatAnnotation - { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; + activity?.SetError(ex); } - } - catch (ClientResultException ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - var tokenUsage = response?.Value?.Usage; - var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, @@ -167,7 +183,7 @@ public async Task GetChatCompletionsAsync(Agent agent, var tokenUsage = response?.Value?.Usage; var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - + // After chat completion hook foreach (var hook in hooks) { diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index 6349b1ed0..4323118ac 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; @@ -10,6 +11,7 @@ using BotSharp.Plugin.DeepSeek.Providers; using Microsoft.Extensions.Logging; using OpenAI.Chat; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.DeepSeekAI.Providers.Chat; @@ -17,6 +19,7 @@ public class ChatCompletionProvider : IChatCompletion { protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; @@ -25,15 +28,18 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( IServiceProvider services, + ITelemetryService telemetryService, ILogger logger) { _services = services; + _telemetryService = telemetryService; _logger = logger; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -44,68 +50,75 @@ public async Task GetChatCompletions(Agent agent, List new ChatAnnotation + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() - }; - } + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } - var tokenUsage = response?.Value?.Usage; - var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; } - - return responseMessage; } public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj new file mode 100644 index 000000000..8d73c6489 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj @@ -0,0 +1,31 @@ + + + $(TargetFramework) + enable + enable + $(LangVersion) + true + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + true + $(SolutionDir)packages + + + + + false + runtime + + + + + + PreserveNewest + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs new file mode 100644 index 000000000..ef9686482 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.GiteeAI.Providers.Chat; +using BotSharp.Plugin.GiteeAI.Providers.Embedding; + +namespace BotSharp.Plugin.GiteeAI; + +public class GiteeAiPlugin : IBotSharpPlugin +{ + public string Id => "59ad4c3c-0b88-3344-ba99-5245ec015938"; + public string Name => "GiteeAI"; + public string Description => "Gitee AI"; + public string IconUrl => "https://ai-assets.gitee.com/_next/static/media/gitee-ai.622edfb0.ico"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs new file mode 100644 index 000000000..96152c053 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -0,0 +1,497 @@ +using BotSharp.Abstraction.Agents.Constants; +using BotSharp.Abstraction.Diagnostics.Telemetry; +using BotSharp.Abstraction.Files; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; + +namespace BotSharp.Plugin.GiteeAI.Providers.Chat; + +/// +/// 模力方舟的文本对话 +/// +public class ChatCompletionProvider( + ILogger logger, + ITelemetryService telemetryService, + IServiceProvider services) : IChatCompletion +{ + protected string _model = string.Empty; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + + public async Task GetChatCompletions(Agent agent, List conversations) + { + var contentHooks = services.GetServices().ToList(); + var convService = services.GetService(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var client = ProviderHelper.GetClient(Provider, _model, services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + using (var activity = telemetryService.StartCompletionActivity(null, _model, Provider, conversations, convService)) + { + var response = chatClient.CompleteChat(messages, options); + var value = response.Value; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; + + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + + RoleDialogModel responseMessage; + if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + { + var toolCall = value.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.FunctionName, + FunctionArgs = toolCall?.FunctionArguments?.ToString() + }; + + // Somethings LLM will generate a function name with agent name. + if (!string.IsNullOrEmpty(responseMessage.FunctionName)) + { + responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); + } + } + else + { + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + }; + } + + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; + } + } + + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onStreamResponseReceived) + { + var contentHooks = services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + var client = ProviderHelper.GetClient(Provider, _model, services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = chatClient.CompleteChatStreamingAsync(messages, options); + + await foreach (var choice in response) + { + TrackStreamingToolingUpdate(choice.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + if (!choice.ContentUpdate.IsNullOrEmpty() && choice.ContentUpdate[0] != null) + { + foreach (var contentPart in choice.ContentUpdate) + { + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } + } + + logger.LogInformation(choice.ContentUpdate[0]?.Text); + + if (!string.IsNullOrEmpty(choice.ContentUpdate[0]?.Text)) + { + var msg = new RoleDialogModel(choice.Role?.ToString() ?? ChatMessageRole.Assistant.ToString(), choice.ContentUpdate[0]?.Text ?? string.Empty); + + await onStreamResponseReceived(msg); + } + } + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + RoleDialogModel responseMessage = new(ChatMessageRole.Assistant.ToString(), content); + + var tools = ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + foreach (var tool in tools) + { + tool.CurrentAgentId = agent.Id; + tool.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; + await onStreamResponseReceived(tool); + } + + if (tools.Length > 0) + { + responseMessage = tools[0]; + } + + return responseMessage; + } + + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) + { + var hooks = services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in hooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var client = ProviderHelper.GetClient(Provider, _model, services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = await chatClient.CompleteChatAsync(messages, options); + var value = response.Value; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; + + var msg = new RoleDialogModel(AgentRole.Assistant, text) + { + CurrentAgentId = agent.Id + }; + + // After chat completion hook + foreach (var hook in hooks) + { + await hook.AfterGenerated(msg, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + + if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + { + var toolCall = value.ToolCalls?.FirstOrDefault(); + logger.LogInformation($"[{agent.Name}]: {toolCall?.FunctionName}({toolCall?.FunctionArguments})"); + + var funcContextIn = new RoleDialogModel(AgentRole.Function, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.FunctionName, + FunctionArgs = toolCall?.FunctionArguments?.ToString() + }; + + // Somethings LLM will generate a function name with agent name. + if (!string.IsNullOrEmpty(funcContextIn.FunctionName)) + { + funcContextIn.FunctionName = funcContextIn.FunctionName.Split('.').Last(); + } + + // Execute functions + await onFunctionExecuting(funcContextIn); + } + else + { + // Text response received + await onMessageReceived(msg); + } + + return true; + } + + public async Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived) + { + var client = ProviderHelper.GetClient(Provider, _model, services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = chatClient.CompleteChatStreamingAsync(messages, options); + + await foreach (var choice in response) + { + if (choice.FinishReason == ChatFinishReason.FunctionCall || choice.FinishReason == ChatFinishReason.ToolCalls) + { + var update = choice.ToolCallUpdates?.FirstOrDefault()?.FunctionArgumentsUpdate?.ToString() ?? string.Empty; + logger.LogInformation(update); + + await onMessageReceived(new RoleDialogModel(AgentRole.Assistant, update)); + continue; + } + + if (choice.ContentUpdate.IsNullOrEmpty()) continue; + + logger.LogInformation(choice.ContentUpdate[0]?.Text); + + await onMessageReceived(new RoleDialogModel(choice.Role?.ToString() ?? ChatMessageRole.Assistant.ToString(), choice.ContentUpdate[0]?.Text ?? string.Empty)); + } + + return true; + } + + public void SetModelName(string model) + { + _model = model; + } + + protected (string, IEnumerable, ChatCompletionOptions) PrepareOptions(Agent agent, List conversations) + { + var agentService = services.GetRequiredService(); + var state = services.GetRequiredService(); + var fileStorage = services.GetRequiredService(); + var settingsService = services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + var allowMultiModal = settings != null && settings.MultiModal; + + var messages = new List(); + float? temperature = float.Parse(state.GetState("temperature", "0.0")); + var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens) + ? tokens + : agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN; + + + state.SetState("temperature", temperature.ToString()); + state.SetState("max_tokens", maxTokens.ToString()); + + var options = new ChatCompletionOptions() + { + Temperature = temperature, + MaxOutputTokenCount = maxTokens + }; + + var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []); + foreach (var function in functions) + { + if (!agentService.RenderFunction(agent, function)) continue; + + var property = agentService.RenderFunctionProperty(agent, function); + + options.Tools.Add(ChatTool.CreateFunctionTool( + functionName: function.Name, + functionDescription: function.Description, + functionParameters: BinaryData.FromObjectAsJson(property))); + } + + if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) + { + var text = agentService.RenderInstruction(agent); + messages.Add(new SystemChatMessage(text)); + } + + if (!string.IsNullOrEmpty(agent.Knowledges)) + { + messages.Add(new SystemChatMessage(agent.Knowledges)); + } + + var filteredMessages = conversations.Select(x => x).ToList(); + var firstUserMsgIdx = filteredMessages.FindIndex(x => x.Role == AgentRole.User); + if (firstUserMsgIdx > 0) + { + filteredMessages = filteredMessages.Where((_, idx) => idx >= firstUserMsgIdx).ToList(); + } + + foreach (var message in filteredMessages) + { + if (message.Role == AgentRole.Function) + { + messages.Add(new AssistantChatMessage(new List + { + ChatToolCall.CreateFunctionToolCall(message.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + })); + + messages.Add(new ToolChatMessage(message.FunctionName, message.Content)); + } + else if (message.Role == AgentRole.User) + { + var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; + messages.Add(new UserChatMessage(text)); + } + else if (message.Role == AgentRole.Assistant) + { + messages.Add(new AssistantChatMessage(message.Content)); + } + } + + var prompt = GetPrompt(messages, options); + return (prompt, messages, options); + } + + private string GetPrompt(IEnumerable messages, ChatCompletionOptions options) + { + var prompt = string.Empty; + + if (!messages.IsNullOrEmpty()) + { + // System instruction + var verbose = string.Join("\r\n", messages + .Select(x => x as SystemChatMessage) + .Where(x => x != null) + .Select(x => + { + if (!string.IsNullOrEmpty(x.ParticipantName)) + { + // To display Agent name in log + return $"[{x.ParticipantName}]: {x.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + return $"{AgentRole.System}: {x.Content.FirstOrDefault()?.Text ?? string.Empty}"; + })); + prompt += $"{verbose}\r\n"; + + prompt += "\r\n[CONVERSATION]"; + verbose = string.Join("\r\n", messages + .Where(x => x as SystemChatMessage == null) + .Select(x => + { + var fnMessage = x as ToolChatMessage; + if (fnMessage != null) + { + return $"{AgentRole.Function}: {fnMessage.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + + var userMessage = x as UserChatMessage; + if (userMessage != null) + { + var content = x.Content.FirstOrDefault()?.Text ?? string.Empty; + return !string.IsNullOrEmpty(userMessage.ParticipantName) && userMessage.ParticipantName != "route_to_agent" ? + $"{userMessage.ParticipantName}: {content}" : + $"{AgentRole.User}: {content}"; + } + + var assistMessage = x as AssistantChatMessage; + if (assistMessage != null) + { + var toolCall = assistMessage.ToolCalls?.FirstOrDefault(); + return toolCall != null ? + $"{AgentRole.Assistant}: Call function {toolCall?.FunctionName}({toolCall?.FunctionArguments})" : + $"{AgentRole.Assistant}: {assistMessage.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + + return string.Empty; + })); + prompt += $"\r\n{verbose}\r\n"; + } + + if (!options.Tools.IsNullOrEmpty()) + { + var functions = string.Join("\r\n", options.Tools.Select(fn => + { + return $"\r\n{fn.FunctionName}: {fn.FunctionDescription}\r\n{fn.FunctionParameters}"; + })); + prompt += $"\r\n[FUNCTIONS]{functions}\r\n"; + } + + return prompt; + } + + private static void TrackStreamingToolingUpdate( + IReadOnlyList? updates, + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + if (updates is null) + { + // Nothing to track. + return; + } + + foreach (var update in updates) + { + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (!string.IsNullOrWhiteSpace(update.ToolCallId)) + { + (toolCallIdsByIndex ??= [])[update.Index] = update.ToolCallId; + } + + // Ensure we're tracking the function's name. + if (!string.IsNullOrWhiteSpace(update.FunctionName)) + { + (functionNamesByIndex ??= [])[update.Index] = update.FunctionName; + } + + // Ensure we're tracking the function's arguments. + if (update.FunctionArgumentsUpdate is not null && !update.FunctionArgumentsUpdate.ToMemory().IsEmpty) + { + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) + { + functionArgumentBuildersByIndex[update.Index] = arguments = new(); + } + + arguments.Append(update.FunctionArgumentsUpdate.ToString()); + } + } + } + + private static RoleDialogModel[] ConvertToolCallUpdatesToFunctionToolCalls( + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + RoleDialogModel[] toolCalls = []; + if (toolCallIdsByIndex is { Count: > 0 }) + { + toolCalls = new RoleDialogModel[toolCallIdsByIndex.Count]; + + int i = 0; + foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) + { + string? functionName = null; + StringBuilder? functionArguments = null; + + functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); + functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); + + toolCalls[i] = new RoleDialogModel(AgentRole.Function, string.Empty) + { + FunctionName = functionName ?? string.Empty, + FunctionArgs = functionArguments?.ToString() ?? string.Empty, + }; + i++; + } + + Debug.Assert(i == toolCalls.Length); + } + + return toolCalls; + } + +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs new file mode 100644 index 000000000..80a8dbd71 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Logging; +using OpenAI.Embeddings; + +namespace BotSharp.Plugin.GiteeAI.Providers.Embedding; + +public class TextEmbeddingProvider( + ILogger logger, + IServiceProvider services) : ITextEmbedding +{ + protected readonly IServiceProvider _services = services; + protected readonly ILogger _logger = logger; + + private const int DEFAULT_DIMENSION = 1024; + protected string _model = "bge-m3"; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + protected int _dimension; + + public async Task GetVectorAsync(string text) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingAsync(text, options); + var value = response.Value; + return value.ToFloats().ToArray(); + } + + public async Task> GetVectorsAsync(List texts) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingsAsync(texts, options); + var value = response.Value; + return value.Select(x => x.ToFloats().ToArray()).ToList(); + } + + public void SetModelName(string model) + { + _model = model; + } + + private EmbeddingGenerationOptions PrepareOptions() + { + return new EmbeddingGenerationOptions + { + Dimensions = GetDimension() + }; + } + + public int GetDimension() + { + var state = _services.GetRequiredService(); + var stateDimension = state.GetState("embedding_dimension"); + var defaultDimension = _dimension > 0 ? _dimension : DEFAULT_DIMENSION; + + if (int.TryParse(stateDimension, out var dimension)) + { + return dimension > 0 ? dimension : defaultDimension; + } + return defaultDimension; + } + + public void SetDimension(int dimension) + { + _dimension = dimension > 0 ? dimension : DEFAULT_DIMENSION; + } + +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs new file mode 100644 index 000000000..b532e834c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs @@ -0,0 +1,16 @@ +using OpenAI; +using System.ClientModel; + +namespace BotSharp.Plugin.GiteeAI.Providers; + +public static class ProviderHelper +{ + public static OpenAIClient GetClient(string provider, string model, IServiceProvider services) + { + var settingsService = services.GetRequiredService(); + var settings = settingsService.GetSetting(provider, model); + var options = !string.IsNullOrEmpty(settings.Endpoint) ? + new OpenAIClientOptions { Endpoint = new Uri(settings.Endpoint) } : null; + return new OpenAIClient(new ApiKeyCredential(settings.ApiKey), options); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/README.md b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md new file mode 100644 index 000000000..5b4d00ff4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md @@ -0,0 +1,8 @@ +Model Ark (Gitee AI) , hereinafter referred to as Gitee AI, aggregates the latest and most popular AI models, providing a one-stop service for model experience, inference, fine-tuning, and application deployment . We offer a diverse range of computing power options, aiming to help enterprises and developers build AI applications more easily . +ChatCompletions Interface: + +- https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/post/chat/completions + +Signature Authentication Method: + +- https://ai.gitee.com/docs/organization/access-token \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs new file mode 100644 index 000000000..aa44ad1e2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs @@ -0,0 +1,15 @@ +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Loggers; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Utilities; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs index d224fb122..44563fa44 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; @@ -5,6 +6,7 @@ using GenerativeAI; using GenerativeAI.Core; using GenerativeAI.Types; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.GoogleAi.Providers.Chat; @@ -12,6 +14,8 @@ public class ChatCompletionProvider : IChatCompletion { private readonly IServiceProvider _services; private readonly ILogger _logger; + + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; private string _model; @@ -22,10 +26,12 @@ public class ChatCompletionProvider : IChatCompletion private GoogleAiSettings _settings; public ChatCompletionProvider( IServiceProvider services, + ITelemetryService telemetryService, GoogleAiSettings googleSettings, ILogger logger) { _settings = googleSettings; + _telemetryService = telemetryService; _services = services; _logger = logger; } @@ -33,6 +39,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -43,49 +50,58 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index 7ab149e26..c7a9cd36c 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,9 +1,12 @@ #pragma warning disable OPENAI001 +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; using BotSharp.Core.MessageHub; +using Microsoft.AspNetCore.Cors.Infrastructure; using OpenAI.Chat; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.OpenAI.Providers.Chat; @@ -12,6 +15,7 @@ public class ChatCompletionProvider : IChatCompletion protected readonly OpenAiSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; protected string _model; private List renderedInstructions = []; @@ -22,16 +26,19 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( OpenAiSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -42,68 +49,77 @@ public async Task GetChatCompletions(Agent agent, List new ChatAnnotation + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() - }; - } + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } - var tokenUsage = response.Value?.Usage; - var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; + var tokenUsage = response.Value?.Usage; + var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs new file mode 100644 index 000000000..1e183a5dd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace BotSharp.Plugin.PythonInterpreter.Interfaces; + +public interface IPyScriptRunner +{ + Task RunScript(string scriptPath, string args); +} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs index 0ca84c3ff..c2f092637 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Python.Runtime; using System.IO; +using BotSharp.Plugin.PythonInterpreter.Interfaces; namespace BotSharp.Plugin.PythonInterpreter; @@ -20,9 +21,10 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) var settings = new PythonInterpreterSettings(); config.Bind("PythonInterpreter", settings); services.AddSingleton(x => settings); - + services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public void Configure(IApplicationBuilder app) diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs new file mode 100644 index 000000000..78393e931 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using BotSharp.Plugin.PythonInterpreter.Interfaces; +using BotSharp.Plugin.PythonInterpreter.Settings; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.PythonInterpreter.Services; + +public class PyScriptRunner : IPyScriptRunner +{ + private readonly PythonInterpreterSettings _settings; + private readonly ILogger _logger; + + public PyScriptRunner(PythonInterpreterSettings settings, ILogger logger) + { + _settings = settings; + _logger = logger; + } + + public async Task RunScript(string scriptPath, string args) + { + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException($"Python script not found: {scriptPath}"); + } + + var cmd = _settings.PythonVersion == "python3" ? "python3" : "python"; + // 允许配置绝对路径 + if (!string.IsNullOrEmpty(_settings.InstallLocation)) + { + cmd = Path.Combine(_settings.InstallLocation, cmd); + } + + var processStartInfo = new ProcessStartInfo + { + FileName = cmd, + Arguments = $"\"{scriptPath}\" {args}", // 注意参数转义,特别是包含空格的路径 + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using var process = Process.Start(processStartInfo); + if (process == null) throw new Exception("Failed to start python process."); + + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + var output = await outputTask; + var error = await errorTask; + + if (process.ExitCode != 0) + { + _logger.LogError($"Python script execution failed. Exit code: {process.ExitCode}. Error: {error}"); + throw new Exception($"Script exited with code {process.ExitCode}: {error}"); + } + + return output; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running python script."); + throw; + } + } +} diff --git a/src/WebStarter/Program.cs b/src/WebStarter/Program.cs index 2c9c073c2..087d915fc 100644 --- a/src/WebStarter/Program.cs +++ b/src/WebStarter/Program.cs @@ -1,11 +1,10 @@ +using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core; using BotSharp.Core.MCP; -using BotSharp.OpenAPI; using BotSharp.Logger; +using BotSharp.OpenAPI; using BotSharp.Plugin.ChatHub; using Serilog; -using BotSharp.Abstraction.Messaging.JsonConverters; -using StackExchange.Redis; var builder = WebApplication.CreateBuilder(args); @@ -31,7 +30,7 @@ }).AddBotSharpOpenAPI(builder.Configuration, allowedOrigins, builder.Environment, true) .AddBotSharpMCP(builder.Configuration) .AddBotSharpLogger(builder.Configuration); - + // Add service defaults & Aspire components. builder.AddServiceDefaults(); diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index c49e28cfc..79cebe2a5 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -36,10 +36,12 @@ + - + + @@ -83,6 +85,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index a97667e9e..c8fecf358 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -6,6 +6,8 @@ } }, "AllowedHosts": "*", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_SERVICE_NAME": "apiservice", "AllowedOrigins": [ "http://localhost:5015", "http://0.0.0.0:5015", @@ -57,22 +59,15 @@ ] }, { - "Name": "gpt-35-turbo-instruct", - "Version": "0914", - "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", - "Type": "text", - "Capabilities": [ - "Text" - ], - "Cost": { - "TextInputCost": 0.0015, - "CachedTextInputCost": 0, - "AudioInputCost": 0, - "CachedAudioInputCost": 0, - "TextOutputCost": 0.002, - "AudioOutputCost": 0 - } + "Id": "gpt-4.1-mini", + "Name": "gpt-4.1-mini", + "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", + "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 } ] }, @@ -493,6 +488,43 @@ } } ] + }, + { + "Provider": "gitee-ai", + "Models": [ + { + "Name": "mimo-v2-flash", + "ApiKey": "sk-CXttCsbEbvEzW9dGmur4bEfXB5Tog9Bi4krP", + "Endpoint": "http://182.254.176.239/v1", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Name": "GLM-4_5", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Id": "bge-m3", + "Name": "bge-m3", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/embeddings/", + "Type": "embedding", + "Dimension": 1024, + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 + } + ] } ], @@ -509,8 +541,8 @@ "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", "EnableTranslator": false, "LlmConfig": { - "Provider": "openai", - "Model": "gpt-4.1-nano" + "Provider": "gitee-ai", + "Model": "mimo-v2-flash" } }, @@ -855,7 +887,14 @@ "Language": "en" } }, - + "Langfuse": { + "PublicKey": "pk-lf-a03b0da6-c0f4-4805-959a-fe359a60cea5", + "SecretKey": "sk-lf-074f71f9-8b4b-4f08-8e5e-ebe4dd3fff02", + "BaseUrl": "https://us.cloud.langfuse.com/" + }, + "AgentSkills": { + "DataDir": "C:/workshop/github/BotSharp/skills/test-skill" + }, "PluginLoader": { "Assemblies": [ "BotSharp.Core", @@ -871,6 +910,7 @@ "BotSharp.Plugin.GoogleAI", "BotSharp.Plugin.MetaAI", "BotSharp.Plugin.DeepSeekAI", + "BotSharp.Plugin.GiteeAI", "BotSharp.Plugin.MetaMessenger", "BotSharp.Plugin.HuggingFace", "BotSharp.Plugin.KnowledgeBase", @@ -896,7 +936,8 @@ "BotSharp.Plugin.SqlDriver", "BotSharp.Plugin.TencentCos", "BotSharp.Plugin.PythonInterpreter", - "BotSharp.Plugin.FuzzySharp" + "BotSharp.Plugin.FuzzySharp", + "BotSharp.Plugin.AgentSkills" ] } } diff --git a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj index 4e335c45b..a686c8ed6 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -8,10 +8,7 @@ - - - diff --git a/tests/UnitTest/AgentSkillsTest.cs b/tests/UnitTest/AgentSkillsTest.cs new file mode 100644 index 000000000..19b930f4d --- /dev/null +++ b/tests/UnitTest/AgentSkillsTest.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.AgentSkills.Settings; +using Xunit; +using Assert = Xunit.Assert; + +namespace BotSharp.UnitTest +{ + public class AgentSkillsTest + { + [Fact] + public async Task TestGetAvailableSkills() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), "BotSharpTests", "Skills"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + // Create a dummy SKILL.md + var skillDir = Path.Combine(tempDir, "TestSkill"); + Directory.CreateDirectory(skillDir); + var skillFile = Path.Combine(skillDir, "SKILL.md"); + var content = @"--- +name: TestSkill +description: This is a test skill +version: 1.0.0 +--- +System Prompt"; + await File.WriteAllTextAsync(skillFile, content); + + // Mock Settings and Logger + var settings = new AgentSkillsSettings { DataDir = tempDir }; + var loggerMock = new Mock>(); + + // Act + var service = new AgentSkillService(settings, loggerMock.Object); + // Service constructor calls RefreshSkills().Wait(), so skils should be loaded. + + var skills = await service.GetAvailableSkills(); + + // Assert + Assert.Single(skills); + Assert.Equal("TestSkill", skills.First().Name); + Assert.Equal("This is a test skill", skills.First().Description); + + // Cleanup + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + + [Fact] + public async Task TestGetSkillDetails() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), "BotSharpTests", "Skills2"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + // Create a dummy SKILL.md + var skillDir = Path.Combine(tempDir, "MathSkill"); + Directory.CreateDirectory(skillDir); + var skillFile = Path.Combine(skillDir, "SKILL.md"); + var content = @"--- +name: MathSkill +description: Performs math operations +version: 1.0.0 +--- +You are a math expert."; + await File.WriteAllTextAsync(skillFile, content); + + // Mock Settings and Logger + var settings = new AgentSkillsSettings { DataDir = tempDir }; + var loggerMock = new Mock>(); + + var service = new AgentSkillService(settings, loggerMock.Object); + + // Act + var skill = await service.GetSkill("MathSkill"); + + // Assert + Assert.NotNull(skill); + Assert.Equal("MathSkill", skill.Name); + Assert.Equal("You are a math expert.", skill.MarkdownBody); + + // Cleanup + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } +} diff --git a/tests/UnitTest/UnitTest.csproj b/tests/UnitTest/UnitTest.csproj index 401b2e5e9..3ea815f19 100644 --- a/tests/UnitTest/UnitTest.csproj +++ b/tests/UnitTest/UnitTest.csproj @@ -11,16 +11,22 @@ + - all runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + +