This is page 98 of 145. Use http://codebase.md/microsoft/semanticworkbench?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ ├── .vscode
│ │ └── settings.json
│ ├── devcontainer.json
│ ├── OPTIMIZING_FOR_CODESPACES.md
│ ├── POST_SETUP_README.md
│ └── README.md
├── .dockerignore
├── .gitattributes
├── .github
│ ├── policheck.yml
│ └── workflows
│ ├── assistants-codespace-assistant.yml
│ ├── assistants-document-assistant.yml
│ ├── assistants-explorer-assistant.yml
│ ├── assistants-guided-conversation-assistant.yml
│ ├── assistants-knowledge-transfer-assistant.yml
│ ├── assistants-navigator-assistant.yml
│ ├── assistants-project-assistant.yml
│ ├── assistants-prospector-assistant.yml
│ ├── assistants-skill-assistant.yml
│ ├── libraries.yml
│ ├── mcp-server-giphy.yml
│ ├── mcp-server-memory-filesystem-edit.yml
│ ├── mcp-server-memory-user-bio.yml
│ ├── mcp-server-memory-whiteboard.yml
│ ├── mcp-server-open-deep-research-clone.yml
│ ├── mcp-server-web-research.yml
│ ├── workbench-app.yml
│ └── workbench-service.yml
├── .gitignore
├── .multi-root-tools
│ ├── Makefile
│ └── README.md
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── ai_context
│ └── generated
│ ├── ASPIRE_ORCHESTRATOR.md
│ ├── ASSISTANT_CODESPACE.md
│ ├── ASSISTANT_DOCUMENT.md
│ ├── ASSISTANT_NAVIGATOR.md
│ ├── ASSISTANT_PROJECT.md
│ ├── ASSISTANT_PROSPECTOR.md
│ ├── ASSISTANTS_OTHER.md
│ ├── ASSISTANTS_OVERVIEW.md
│ ├── CONFIGURATION.md
│ ├── DOTNET_LIBRARIES.md
│ ├── EXAMPLES.md
│ ├── MCP_SERVERS.md
│ ├── PYTHON_LIBRARIES_AI_CLIENTS.md
│ ├── PYTHON_LIBRARIES_CORE.md
│ ├── PYTHON_LIBRARIES_EXTENSIONS.md
│ ├── PYTHON_LIBRARIES_SKILLS.md
│ ├── PYTHON_LIBRARIES_SPECIALIZED.md
│ ├── TOOLS.md
│ ├── WORKBENCH_FRONTEND.md
│ └── WORKBENCH_SERVICE.md
├── aspire-orchestrator
│ ├── .editorconfig
│ ├── Aspire.AppHost
│ │ ├── .gitignore
│ │ ├── appsettings.json
│ │ ├── Aspire.AppHost.csproj
│ │ ├── Program.cs
│ │ └── Properties
│ │ └── launchSettings.json
│ ├── Aspire.Extensions
│ │ ├── Aspire.Extensions.csproj
│ │ ├── Dashboard.cs
│ │ ├── DockerFileExtensions.cs
│ │ ├── PathNormalizer.cs
│ │ ├── UvAppHostingExtensions.cs
│ │ ├── UvAppResource.cs
│ │ ├── VirtualEnvironment.cs
│ │ └── WorkbenchServiceHostingExtensions.cs
│ ├── Aspire.ServiceDefaults
│ │ ├── Aspire.ServiceDefaults.csproj
│ │ └── Extensions.cs
│ ├── README.md
│ ├── run.sh
│ ├── SemanticWorkbench.Aspire.sln
│ └── SemanticWorkbench.Aspire.sln.DotSettings
├── assistants
│ ├── codespace-assistant
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ ├── icon_context_transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── models.py
│ │ │ │ ├── request_builder.py
│ │ │ │ ├── response.py
│ │ │ │ ├── step_handler.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── abbreviations.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ └── openai_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── card_content_context_transfer.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── codespace_assistant_info.md
│ │ │ │ ├── context_transfer_assistant_info.md
│ │ │ │ ├── guardrails_prompt.txt
│ │ │ │ ├── guidance_prompt_context_transfer.txt
│ │ │ │ ├── guidance_prompt.txt
│ │ │ │ ├── instruction_prompt_context_transfer.txt
│ │ │ │ └── instruction_prompt.txt
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── document-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── context_management
│ │ │ │ ├── __init__.py
│ │ │ │ └── inspector.py
│ │ │ ├── filesystem
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _convert.py
│ │ │ │ ├── _file_sources.py
│ │ │ │ ├── _filesystem.py
│ │ │ │ ├── _inspector.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _prompts.py
│ │ │ │ └── _tasks.py
│ │ │ ├── guidance
│ │ │ │ ├── __init__.py
│ │ │ │ ├── dynamic_ui_inspector.py
│ │ │ │ ├── guidance_config.py
│ │ │ │ ├── guidance_prompts.py
│ │ │ │ └── README.md
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── models.py
│ │ │ │ ├── prompts.py
│ │ │ │ ├── responder.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ ├── openai_utils.py
│ │ │ │ ├── tokens_tiktoken.py
│ │ │ │ └── workbench_messages.py
│ │ │ ├── text_includes
│ │ │ │ └── document_assistant_info.md
│ │ │ ├── types.py
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── assistant.code-workspace
│ │ ├── CLAUDE.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_convert.py
│ │ │ └── test_data
│ │ │ ├── blank_image.png
│ │ │ ├── Formatting Test.docx
│ │ │ ├── sample_data.csv
│ │ │ ├── sample_data.xlsx
│ │ │ ├── sample_page.html
│ │ │ ├── sample_presentation.pptx
│ │ │ └── simple_pdf.pdf
│ │ └── uv.lock
│ ├── explorer-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── model.py
│ │ │ │ ├── response_anthropic.py
│ │ │ │ ├── response_openai.py
│ │ │ │ └── response.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── guided-conversation-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agents
│ │ │ │ ├── guided_conversation
│ │ │ │ │ ├── config.py
│ │ │ │ │ ├── definition.py
│ │ │ │ │ └── definitions
│ │ │ │ │ ├── er_triage.py
│ │ │ │ │ ├── interview.py
│ │ │ │ │ ├── patient_intake.py
│ │ │ │ │ └── poem_feedback.py
│ │ │ │ └── guided_conversation_agent.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── knowledge-transfer-assistant
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agentic
│ │ │ │ ├── __init__.py
│ │ │ │ ├── analysis.py
│ │ │ │ ├── coordinator_support.py
│ │ │ │ └── team_welcome.py
│ │ │ ├── assets
│ │ │ │ ├── icon-knowledge-transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── assistant.py
│ │ │ ├── common.py
│ │ │ ├── config.py
│ │ │ ├── conversation_clients.py
│ │ │ ├── conversation_share_link.py
│ │ │ ├── data.py
│ │ │ ├── domain
│ │ │ │ ├── __init__.py
│ │ │ │ ├── audience_manager.py
│ │ │ │ ├── information_request_manager.py
│ │ │ │ ├── knowledge_brief_manager.py
│ │ │ │ ├── knowledge_digest_manager.py
│ │ │ │ ├── learning_objectives_manager.py
│ │ │ │ └── share_manager.py
│ │ │ ├── files.py
│ │ │ ├── logging.py
│ │ │ ├── notifications.py
│ │ │ ├── respond.py
│ │ │ ├── storage_models.py
│ │ │ ├── storage.py
│ │ │ ├── string_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── assistant_info.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── coordinator_instructions.txt
│ │ │ │ ├── coordinator_role.txt
│ │ │ │ ├── knowledge_digest_instructions.txt
│ │ │ │ ├── knowledge_digest_prompt.txt
│ │ │ │ ├── share_information_request_detection.txt
│ │ │ │ ├── team_instructions.txt
│ │ │ │ ├── team_role.txt
│ │ │ │ └── welcome_message_generation.txt
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── information_requests.py
│ │ │ │ ├── learning_objectives.py
│ │ │ │ ├── learning_outcomes.py
│ │ │ │ ├── progress_tracking.py
│ │ │ │ └── share_setup.py
│ │ │ ├── ui_tabs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── brief.py
│ │ │ │ ├── common.py
│ │ │ │ ├── debug.py
│ │ │ │ ├── learning.py
│ │ │ │ └── sharing.py
│ │ │ └── utils.py
│ │ ├── CLAUDE.md
│ │ ├── docs
│ │ │ ├── design
│ │ │ │ ├── actions.md
│ │ │ │ └── inference.md
│ │ │ ├── DEV_GUIDE.md
│ │ │ ├── how-kta-works.md
│ │ │ ├── JTBD.md
│ │ │ ├── knowledge-transfer-goals.md
│ │ │ ├── learning_assistance.md
│ │ │ ├── notable_claude_conversations
│ │ │ │ ├── clarifying_quad_modal_design.md
│ │ │ │ ├── CLAUDE_PROMPTS.md
│ │ │ │ ├── transfer_state.md
│ │ │ │ └── trying_the_context_agent.md
│ │ │ └── opportunities-of-knowledge-transfer.md
│ │ ├── knowledge-transfer-assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_artifact_loading.py
│ │ │ ├── test_inspector.py
│ │ │ ├── test_share_manager.py
│ │ │ ├── test_share_storage.py
│ │ │ ├── test_share_tools.py
│ │ │ └── test_team_mode.py
│ │ └── uv.lock
│ ├── Makefile
│ ├── navigator-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ ├── card_content.md
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── completion_requestor.py
│ │ │ │ ├── local_tool
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── add_assistant_to_conversation.py
│ │ │ │ │ ├── list_assistant_services.py
│ │ │ │ │ └── model.py
│ │ │ │ ├── models.py
│ │ │ │ ├── prompt.py
│ │ │ │ ├── request_builder.py
│ │ │ │ ├── response.py
│ │ │ │ ├── step_handler.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ ├── openai_utils.py
│ │ │ │ └── tools.py
│ │ │ ├── text_includes
│ │ │ │ ├── guardrails_prompt.md
│ │ │ │ ├── guidance_prompt.md
│ │ │ │ ├── instruction_prompt.md
│ │ │ │ ├── navigator_assistant_info.md
│ │ │ │ └── semantic_workbench_features.md
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── project-assistant
│ │ ├── .cspell
│ │ │ └── custom-dictionary-workspace.txt
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agentic
│ │ │ │ ├── __init__.py
│ │ │ │ ├── act.py
│ │ │ │ ├── coordinator_next_action.py
│ │ │ │ ├── create_invitation.py
│ │ │ │ ├── detect_audience_and_takeaways.py
│ │ │ │ ├── detect_coordinator_actions.py
│ │ │ │ ├── detect_information_request_needs.py
│ │ │ │ ├── detect_knowledge_package_gaps.py
│ │ │ │ ├── focus.py
│ │ │ │ ├── respond.py
│ │ │ │ ├── team_welcome.py
│ │ │ │ └── update_digest.py
│ │ │ ├── assets
│ │ │ │ ├── icon-knowledge-transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── assistant.py
│ │ │ ├── common.py
│ │ │ ├── config.py
│ │ │ ├── conversation_clients.py
│ │ │ ├── data.py
│ │ │ ├── domain
│ │ │ │ ├── __init__.py
│ │ │ │ ├── audience_manager.py
│ │ │ │ ├── conversation_preferences_manager.py
│ │ │ │ ├── information_request_manager.py
│ │ │ │ ├── knowledge_brief_manager.py
│ │ │ │ ├── knowledge_digest_manager.py
│ │ │ │ ├── learning_objectives_manager.py
│ │ │ │ ├── share_manager.py
│ │ │ │ ├── tasks_manager.py
│ │ │ │ └── transfer_manager.py
│ │ │ ├── errors.py
│ │ │ ├── files.py
│ │ │ ├── logging.py
│ │ │ ├── notifications.py
│ │ │ ├── prompt_utils.py
│ │ │ ├── storage.py
│ │ │ ├── string_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── actor_instructions.md
│ │ │ │ ├── assistant_info.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── coordinator_instructions copy.md
│ │ │ │ ├── coordinator_instructions.md
│ │ │ │ ├── create_invitation.md
│ │ │ │ ├── detect_audience.md
│ │ │ │ ├── detect_coordinator_actions.md
│ │ │ │ ├── detect_information_request_needs.md
│ │ │ │ ├── detect_knowledge_package_gaps.md
│ │ │ │ ├── focus.md
│ │ │ │ ├── knowledge_digest_instructions.txt
│ │ │ │ ├── team_instructions.txt
│ │ │ │ ├── to_do.md
│ │ │ │ ├── update_knowledge_brief.md
│ │ │ │ ├── update_knowledge_digest.md
│ │ │ │ └── welcome_message_generation.txt
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── conversation_preferences.py
│ │ │ │ ├── information_requests.py
│ │ │ │ ├── learning_objectives.py
│ │ │ │ ├── learning_outcomes.py
│ │ │ │ ├── progress_tracking.py
│ │ │ │ ├── share_setup.py
│ │ │ │ ├── system_reminders.py
│ │ │ │ ├── tasks.py
│ │ │ │ └── todo.py
│ │ │ ├── ui_tabs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── brief.py
│ │ │ │ ├── common.py
│ │ │ │ ├── debug.py
│ │ │ │ ├── learning.py
│ │ │ │ └── sharing.py
│ │ │ └── utils.py
│ │ ├── CLAUDE.md
│ │ ├── docs
│ │ │ ├── design
│ │ │ │ ├── actions.md
│ │ │ │ ├── control_options.md
│ │ │ │ ├── design.md
│ │ │ │ ├── inference.md
│ │ │ │ └── PXL_20250814_190140267.jpg
│ │ │ ├── DEV_GUIDE.md
│ │ │ ├── how-kta-works.md
│ │ │ ├── JTBD.md
│ │ │ ├── knowledge-transfer-goals.md
│ │ │ ├── learning_assistance.md
│ │ │ ├── notable_claude_conversations
│ │ │ │ ├── clarifying_quad_modal_design.md
│ │ │ │ ├── CLAUDE_PROMPTS.md
│ │ │ │ ├── transfer_state.md
│ │ │ │ └── trying_the_context_agent.md
│ │ │ └── opportunities-of-knowledge-transfer.md
│ │ ├── knowledge-transfer-assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_artifact_loading.py
│ │ │ ├── test_inspector.py
│ │ │ ├── test_share_manager.py
│ │ │ ├── test_share_storage.py
│ │ │ └── test_team_mode.py
│ │ └── uv.lock
│ ├── prospector-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agents
│ │ │ │ ├── artifact_agent.py
│ │ │ │ ├── document
│ │ │ │ │ ├── config.py
│ │ │ │ │ ├── gc_draft_content_feedback_config.py
│ │ │ │ │ ├── gc_draft_outline_feedback_config.py
│ │ │ │ │ ├── guided_conversation.py
│ │ │ │ │ └── state.py
│ │ │ │ └── document_agent.py
│ │ │ ├── artifact_creation_extension
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _llm.py
│ │ │ │ ├── config.py
│ │ │ │ ├── document.py
│ │ │ │ ├── extension.py
│ │ │ │ ├── store.py
│ │ │ │ ├── test
│ │ │ │ │ ├── conftest.py
│ │ │ │ │ ├── evaluation.py
│ │ │ │ │ ├── test_completion_with_tools.py
│ │ │ │ │ └── test_extension.py
│ │ │ │ └── tools.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── form_fill_extension
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── extension.py
│ │ │ │ ├── inspector.py
│ │ │ │ ├── state.py
│ │ │ │ └── steps
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _guided_conversation.py
│ │ │ │ ├── _llm.py
│ │ │ │ ├── acquire_form_step.py
│ │ │ │ ├── extract_form_fields_step.py
│ │ │ │ ├── fill_form_step.py
│ │ │ │ └── types.py
│ │ │ ├── helpers.py
│ │ │ ├── legacy.py
│ │ │ └── text_includes
│ │ │ ├── artifact_agent_enabled.md
│ │ │ ├── guardrails_prompt.txt
│ │ │ ├── guided_conversation_agent_enabled.md
│ │ │ └── skills_agent_enabled.md
│ │ ├── assistant.code-workspace
│ │ ├── gc_learnings
│ │ │ ├── gc_learnings.md
│ │ │ └── images
│ │ │ ├── gc_conversation_plan_fcn.png
│ │ │ ├── gc_conversation_plan_template.png
│ │ │ ├── gc_execute_plan_callstack.png
│ │ │ ├── gc_functions.png
│ │ │ ├── gc_generate_plan_callstack.png
│ │ │ ├── gc_get_resource_instructions.png
│ │ │ ├── gc_get_termination_instructions.png
│ │ │ ├── gc_kernel_arguments.png
│ │ │ ├── gc_plan_calls.png
│ │ │ ├── gc_termination_instructions.png
│ │ │ ├── sk_get_chat_message_contents.png
│ │ │ ├── sk_inner_get_chat_message_contents.png
│ │ │ ├── sk_send_request_prep.png
│ │ │ └── sk_send_request.png
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ └── skill-assistant
│ ├── .env.example
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── assistant
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── logging.py
│ │ ├── skill_assistant.py
│ │ ├── skill_engine_registry.py
│ │ ├── skill_event_mapper.py
│ │ ├── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ └── workbench_helpers.py
│ ├── assistant.code-workspace
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ ├── tests
│ │ └── test_setup.py
│ └── uv.lock
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── docs
│ ├── .vscode
│ │ └── settings.json
│ ├── ASSISTANT_CONFIG.md
│ ├── ASSISTANT_DEVELOPMENT_GUIDE.md
│ ├── CUSTOM_APP_REGISTRATION.md
│ ├── HOSTED_ASSISTANT_WITH_LOCAL_MCP_SERVERS.md
│ ├── images
│ │ ├── architecture-animation.gif
│ │ ├── configure_assistant.png
│ │ ├── conversation_canvas_open.png
│ │ ├── conversation_duplicate.png
│ │ ├── conversation_export.png
│ │ ├── conversation_share_dialog.png
│ │ ├── conversation_share_link.png
│ │ ├── dashboard_configured_view.png
│ │ ├── dashboard_view.png
│ │ ├── license_agreement.png
│ │ ├── message_bar.png
│ │ ├── message_inspection.png
│ │ ├── message_link.png
│ │ ├── new_prospector_assistant_dialog.png
│ │ ├── open_conversation_canvas.png
│ │ ├── prospector_example.png
│ │ ├── readme1.png
│ │ ├── readme2.png
│ │ ├── readme3.png
│ │ ├── rewind.png
│ │ ├── signin_page.png
│ │ └── splash_screen.png
│ ├── LOCAL_ASSISTANT_WITH_REMOTE_WORKBENCH.md
│ ├── SETUP_DEV_ENVIRONMENT.md
│ └── WORKBENCH_APP.md
├── examples
│ ├── dotnet
│ │ ├── .editorconfig
│ │ ├── dotnet-01-echo-bot
│ │ │ ├── appsettings.json
│ │ │ ├── dotnet-01-echo-bot.csproj
│ │ │ ├── MyAgent.cs
│ │ │ ├── MyAgentConfig.cs
│ │ │ ├── MyWorkbenchConnector.cs
│ │ │ ├── Program.cs
│ │ │ └── README.md
│ │ ├── dotnet-02-message-types-demo
│ │ │ ├── appsettings.json
│ │ │ ├── ConnectorExtensions.cs
│ │ │ ├── docs
│ │ │ │ ├── abc.png
│ │ │ │ ├── code.png
│ │ │ │ ├── config.png
│ │ │ │ ├── echo.png
│ │ │ │ ├── markdown.png
│ │ │ │ ├── mermaid.png
│ │ │ │ ├── reverse.png
│ │ │ │ └── safety-check.png
│ │ │ ├── dotnet-02-message-types-demo.csproj
│ │ │ ├── MyAgent.cs
│ │ │ ├── MyAgentConfig.cs
│ │ │ ├── MyWorkbenchConnector.cs
│ │ │ ├── Program.cs
│ │ │ └── README.md
│ │ └── dotnet-03-simple-chatbot
│ │ ├── appsettings.json
│ │ ├── ConnectorExtensions.cs
│ │ ├── dotnet-03-simple-chatbot.csproj
│ │ ├── MyAgent.cs
│ │ ├── MyAgentConfig.cs
│ │ ├── MyWorkbenchConnector.cs
│ │ ├── Program.cs
│ │ └── README.md
│ ├── Makefile
│ └── python
│ ├── python-01-echo-bot
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ └── config.py
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── python-02-simple-chatbot
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ └── python-03-multimodel-chatbot
│ ├── .env.example
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── assistant
│ │ ├── __init__.py
│ │ ├── chat.py
│ │ ├── config.py
│ │ ├── model_adapters.py
│ │ └── text_includes
│ │ └── guardrails_prompt.txt
│ ├── assistant.code-workspace
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ └── uv.lock
├── KNOWN_ISSUES.md
├── libraries
│ ├── dotnet
│ │ ├── .editorconfig
│ │ ├── pack.sh
│ │ ├── README.md
│ │ ├── SemanticWorkbench.sln
│ │ ├── SemanticWorkbench.sln.DotSettings
│ │ └── WorkbenchConnector
│ │ ├── AgentBase.cs
│ │ ├── AgentConfig
│ │ │ ├── AgentConfigBase.cs
│ │ │ ├── AgentConfigPropertyAttribute.cs
│ │ │ └── ConfigUtils.cs
│ │ ├── Constants.cs
│ │ ├── IAgentBase.cs
│ │ ├── icon.png
│ │ ├── Models
│ │ │ ├── Command.cs
│ │ │ ├── Conversation.cs
│ │ │ ├── ConversationEvent.cs
│ │ │ ├── DebugInfo.cs
│ │ │ ├── Insight.cs
│ │ │ ├── Message.cs
│ │ │ ├── MessageMetadata.cs
│ │ │ ├── Participant.cs
│ │ │ ├── Sender.cs
│ │ │ └── ServiceInfo.cs
│ │ ├── Storage
│ │ │ ├── AgentInfo.cs
│ │ │ ├── AgentServiceStorage.cs
│ │ │ └── IAgentServiceStorage.cs
│ │ ├── StringLoggingExtensions.cs
│ │ ├── Webservice.cs
│ │ ├── WorkbenchConfig.cs
│ │ ├── WorkbenchConnector.cs
│ │ └── WorkbenchConnector.csproj
│ ├── Makefile
│ └── python
│ ├── anthropic-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── anthropic_client
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── config.py
│ │ │ └── messages.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── assistant-data-gen
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant_data_gen
│ │ │ ├── __init__.py
│ │ │ ├── assistant_api.py
│ │ │ ├── config.py
│ │ │ ├── gce
│ │ │ │ ├── __init__.py
│ │ │ │ ├── gce_agent.py
│ │ │ │ └── prompts.py
│ │ │ └── pydantic_ai_utils.py
│ │ ├── configs
│ │ │ └── document_assistant_example_config.yaml
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── scripts
│ │ │ ├── gce_simulation.py
│ │ │ └── generate_scenario.py
│ │ └── uv.lock
│ ├── assistant-drive
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ └── settings.json
│ │ ├── assistant_drive
│ │ │ ├── __init__.py
│ │ │ ├── drive.py
│ │ │ └── tests
│ │ │ └── test_basic.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── pytest.ini
│ │ ├── README.md
│ │ ├── usage.ipynb
│ │ └── uv.lock
│ ├── assistant-extensions
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── assistant_extensions
│ │ │ ├── __init__.py
│ │ │ ├── ai_clients
│ │ │ │ └── config.py
│ │ │ ├── artifacts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _artifacts.py
│ │ │ │ ├── _inspector.py
│ │ │ │ └── _model.py
│ │ │ ├── attachments
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _attachments.py
│ │ │ │ ├── _convert.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _shared.py
│ │ │ │ └── _summarizer.py
│ │ │ ├── chat_context_toolkit
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _config.py
│ │ │ │ ├── archive
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _archive.py
│ │ │ │ │ └── _summarizer.py
│ │ │ │ ├── message_history
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _history.py
│ │ │ │ │ └── _message.py
│ │ │ │ └── virtual_filesystem
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _archive_file_source.py
│ │ │ │ └── _attachments_file_source.py
│ │ │ ├── dashboard_card
│ │ │ │ ├── __init__.py
│ │ │ │ └── _dashboard_card.py
│ │ │ ├── document_editor
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _extension.py
│ │ │ │ ├── _inspector.py
│ │ │ │ └── _model.py
│ │ │ ├── mcp
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _assistant_file_resource_handler.py
│ │ │ │ ├── _client_utils.py
│ │ │ │ ├── _devtunnel.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _openai_utils.py
│ │ │ │ ├── _sampling_handler.py
│ │ │ │ ├── _tool_utils.py
│ │ │ │ └── _workbench_file_resource_handler.py
│ │ │ ├── navigator
│ │ │ │ ├── __init__.py
│ │ │ │ └── _navigator.py
│ │ │ └── workflows
│ │ │ ├── __init__.py
│ │ │ ├── _model.py
│ │ │ ├── _workflows.py
│ │ │ └── runners
│ │ │ └── _user_proxy.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ └── attachments
│ │ │ └── test_attachments.py
│ │ └── uv.lock
│ ├── chat-context-toolkit
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.sample
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── assets
│ │ │ ├── archive_v1.png
│ │ │ ├── history_v1.png
│ │ │ └── vfs_v1.png
│ │ ├── chat_context_toolkit
│ │ │ ├── __init__.py
│ │ │ ├── archive
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _archive_reader.py
│ │ │ │ ├── _archive_task_queue.py
│ │ │ │ ├── _state.py
│ │ │ │ ├── _types.py
│ │ │ │ └── summarization
│ │ │ │ ├── __init__.py
│ │ │ │ └── _summarizer.py
│ │ │ ├── history
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _budget.py
│ │ │ │ ├── _decorators.py
│ │ │ │ ├── _history.py
│ │ │ │ ├── _prioritize.py
│ │ │ │ ├── _types.py
│ │ │ │ └── tool_abbreviations
│ │ │ │ ├── __init__.py
│ │ │ │ └── _tool_abbreviations.py
│ │ │ └── virtual_filesystem
│ │ │ ├── __init__.py
│ │ │ ├── _types.py
│ │ │ ├── _virtual_filesystem.py
│ │ │ ├── README.md
│ │ │ └── tools
│ │ │ ├── __init__.py
│ │ │ ├── _ls_tool.py
│ │ │ ├── _tools.py
│ │ │ └── _view_tool.py
│ │ ├── CLAUDE.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ ├── archive
│ │ │ │ └── test_archive_reader.py
│ │ │ ├── history
│ │ │ │ ├── test_abbreviate_messages.py
│ │ │ │ ├── test_history.py
│ │ │ │ ├── test_pair_and_order_tool_messages.py
│ │ │ │ ├── test_prioritize.py
│ │ │ │ └── test_truncate_messages.py
│ │ │ └── virtual_filesystem
│ │ │ ├── test_virtual_filesystem.py
│ │ │ └── tools
│ │ │ ├── test_ls_tool.py
│ │ │ ├── test_tools.py
│ │ │ └── test_view_tool.py
│ │ └── uv.lock
│ ├── content-safety
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── content_safety
│ │ │ ├── __init__.py
│ │ │ ├── evaluators
│ │ │ │ ├── __init__.py
│ │ │ │ ├── azure_content_safety
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── config.py
│ │ │ │ │ └── evaluator.py
│ │ │ │ ├── config.py
│ │ │ │ ├── evaluator.py
│ │ │ │ └── openai_moderations
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ └── evaluator.py
│ │ │ └── README.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── events
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── events
│ │ │ ├── __init__.py
│ │ │ └── events.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── guided-conversation
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── guided_conversation
│ │ │ ├── __init__.py
│ │ │ ├── functions
│ │ │ │ ├── __init__.py
│ │ │ │ ├── conversation_plan.py
│ │ │ │ ├── execution.py
│ │ │ │ └── final_update_plan.py
│ │ │ ├── guided_conversation_agent.py
│ │ │ ├── plugins
│ │ │ │ ├── __init__.py
│ │ │ │ ├── agenda.py
│ │ │ │ └── artifact.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── base_model_llm.py
│ │ │ ├── conversation_helpers.py
│ │ │ ├── openai_tool_calling.py
│ │ │ ├── plugin_helpers.py
│ │ │ └── resources.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── llm-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── llm_client
│ │ │ ├── __init__.py
│ │ │ └── model.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── Makefile
│ ├── mcp-extensions
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_extensions
│ │ │ ├── __init__.py
│ │ │ ├── _client_session.py
│ │ │ ├── _model.py
│ │ │ ├── _sampling.py
│ │ │ ├── _server_extensions.py
│ │ │ ├── _tool_utils.py
│ │ │ ├── llm
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chat_completion.py
│ │ │ │ ├── helpers.py
│ │ │ │ ├── llm_types.py
│ │ │ │ ├── mcp_chat_completion.py
│ │ │ │ └── openai_chat_completion.py
│ │ │ └── server
│ │ │ ├── __init__.py
│ │ │ └── storage.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_tool_utils.py
│ │ └── uv.lock
│ ├── mcp-tunnel
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_tunnel
│ │ │ ├── __init__.py
│ │ │ ├── _devtunnel.py
│ │ │ ├── _dir.py
│ │ │ └── _main.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── openai-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── openai_client
│ │ │ ├── __init__.py
│ │ │ ├── chat_driver
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chat_driver.ipynb
│ │ │ │ ├── chat_driver.py
│ │ │ │ ├── message_history_providers
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── in_memory_message_history_provider.py
│ │ │ │ │ ├── local_message_history_provider.py
│ │ │ │ │ ├── message_history_provider.py
│ │ │ │ │ └── tests
│ │ │ │ │ └── formatted_instructions_test.py
│ │ │ │ └── README.md
│ │ │ ├── client.py
│ │ │ ├── completion.py
│ │ │ ├── config.py
│ │ │ ├── errors.py
│ │ │ ├── logging.py
│ │ │ ├── messages.py
│ │ │ ├── tokens.py
│ │ │ └── tools.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_command_parsing.py
│ │ │ ├── test_formatted_messages.py
│ │ │ ├── test_messages.py
│ │ │ └── test_tokens.py
│ │ └── uv.lock
│ ├── semantic-workbench-api-model
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── semantic_workbench_api_model
│ │ │ ├── __init__.py
│ │ │ ├── assistant_model.py
│ │ │ ├── assistant_service_client.py
│ │ │ ├── workbench_model.py
│ │ │ └── workbench_service_client.py
│ │ └── uv.lock
│ ├── semantic-workbench-assistant
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── semantic_workbench_assistant
│ │ │ ├── __init__.py
│ │ │ ├── assistant_app
│ │ │ │ ├── __init__.py
│ │ │ │ ├── assistant.py
│ │ │ │ ├── config.py
│ │ │ │ ├── content_safety.py
│ │ │ │ ├── context.py
│ │ │ │ ├── error.py
│ │ │ │ ├── export_import.py
│ │ │ │ ├── protocol.py
│ │ │ │ └── service.py
│ │ │ ├── assistant_service.py
│ │ │ ├── auth.py
│ │ │ ├── canonical.py
│ │ │ ├── command.py
│ │ │ ├── config.py
│ │ │ ├── logging_config.py
│ │ │ ├── settings.py
│ │ │ ├── start.py
│ │ │ └── storage.py
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_assistant_app.py
│ │ │ ├── test_canonical.py
│ │ │ ├── test_config.py
│ │ │ └── test_storage.py
│ │ └── uv.lock
│ └── skills
│ ├── .vscode
│ │ └── settings.json
│ ├── Makefile
│ ├── README.md
│ └── skill-library
│ ├── .vscode
│ │ └── settings.json
│ ├── docs
│ │ └── vs-recipe-tool.md
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ ├── skill_library
│ │ ├── __init__.py
│ │ ├── chat_driver_helpers.py
│ │ ├── cli
│ │ │ ├── azure_openai.py
│ │ │ ├── conversation_history.py
│ │ │ ├── README.md
│ │ │ ├── run_routine.py
│ │ │ ├── settings.py
│ │ │ └── skill_logger.py
│ │ ├── engine.py
│ │ ├── llm_info.txt
│ │ ├── logging.py
│ │ ├── README.md
│ │ ├── routine_stack.py
│ │ ├── skill.py
│ │ ├── skills
│ │ │ ├── common
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── bing_search.py
│ │ │ │ ├── consolidate.py
│ │ │ │ ├── echo.py
│ │ │ │ ├── gather_context.py
│ │ │ │ ├── get_content_from_url.py
│ │ │ │ ├── gpt_complete.py
│ │ │ │ ├── select_user_intent.py
│ │ │ │ └── summarize.py
│ │ │ ├── eval
│ │ │ │ ├── __init__.py
│ │ │ │ ├── eval_skill.py
│ │ │ │ └── routines
│ │ │ │ └── eval.py
│ │ │ ├── fabric
│ │ │ │ ├── __init__.py
│ │ │ │ ├── fabric_skill.py
│ │ │ │ ├── patterns
│ │ │ │ │ ├── agility_story
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── ai
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_answers
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_candidates
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_cfp_submission
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_claims
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_comments
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_debate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_email_headers
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_incident
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_interviewer_techniques
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_logs
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_malware
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_military_strategy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_mistakes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_paper
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_patent
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_personality
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_presentation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_product_feedback
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_proposition
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose_json
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose_pinker
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_risk
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_sales_call
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_spiritual_text
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_tech_impact
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_threat_report
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_threat_report_cmds
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_threat_report_trends
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── answer_interview_question
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── ask_secure_by_design_questions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── ask_uncle_duke
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── capture_thinkers_work
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── check_agreement
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── clean_text
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── coding_master
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── compare_and_contrast
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── convert_to_markdown
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_5_sentence_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_academic_paper
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ai_jobs_analysis
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_aphorisms
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_art_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_better_frame
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_coding_project
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_command
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_cyber_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_design_document
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_diy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_formal_email
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_git_diff_commit
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_graph_from_input
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_hormozi_offer
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_idea_compass
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_investigation_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_keynote
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_logo
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_markmap_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_mermaid_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_mermaid_visualization_for_github
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_micro_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_network_threat_landscape
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_newsletter_entry
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_npc
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_pattern
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_prd
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_prediction_block
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_quiz
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_reading_plan
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_recursive_outline
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_report_finding
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_rpg_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_security_update
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_show_intro
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_sigma_rules
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_story_explanation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_stride_threat_model
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_tags
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_threat_scenarios
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ttrc_graph
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ttrc_narrative
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_upgrade_pack
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_user_story
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_video_chapters
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── dialog_with_socrates
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── enrich_blog_post
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_code
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── explain_docs
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── explain_math
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_project
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_terms
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── export_data_as_csv
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_algorithm_update_recommendations
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_article_wisdom
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_book_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_book_recommendations
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_business_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_controversial_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_core_message
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_ctf_writeup
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_domains
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_extraordinary_claims
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_insights
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_insights_dm
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_instructions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_jokes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_latest_video
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_main_idea
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_most_redeeming_thing
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_patterns
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_poc
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_predictions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_primary_problem
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_primary_solution
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_product_features
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_questions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_recipe
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_recommendations
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_references
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_skills
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_song_meaning
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_sponsors
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_videoid
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_wisdom
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_agents
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_dm
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_nometa
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── find_hidden_message
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── find_logical_fallacies
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── get_wow_per_minute
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── get_youtube_rss
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── humanize
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_distinctions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_perspectives
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_relationships
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_systems
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_job_stories
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── improve_academic_writing
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── improve_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── improve_report_finding
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── improve_writing
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── judge_output
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── label_and_rate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── loaded
│ │ │ │ │ ├── md_callout
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── official_pattern_template
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── pattern_explanations.md
│ │ │ │ │ ├── prepare_7s_strategy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── provide_guidance
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_ai_response
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_ai_result
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_content
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── rate_value
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── raw_query
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── raycast
│ │ │ │ │ │ ├── capture_thinkers_work
│ │ │ │ │ │ ├── create_story_explanation
│ │ │ │ │ │ ├── extract_primary_problem
│ │ │ │ │ │ ├── extract_wisdom
│ │ │ │ │ │ └── yt
│ │ │ │ │ ├── recommend_artists
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── recommend_pipeline_upgrades
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── recommend_talkpanel_topics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── refine_design_document
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── review_design
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── sanitize_broken_html_to_markdown
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── show_fabric_options_markmap
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── solve_with_cot
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── stringify
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── suggest_pattern
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── summarize
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_debate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_git_changes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_git_diff
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_lecture
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_legislation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_meeting
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_micro
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_newsletter
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_paper
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_pull-requests
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_rpg_session
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_analyze_challenge_handling
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_check_metrics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_create_h3_career
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_create_opening_sentences
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_describe_life_outlook
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_extract_intro_sentences
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_extract_panel_topics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_blindspots
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_negative_thinking
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_neglected_goals
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_give_encouragement
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_red_team_thinking
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_threat_model_plans
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_visualize_mission_goals_projects
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_year_in_review
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── to_flashcards
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── transcribe_minutes
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── translate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── tweet
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_essay
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_hackerone_report
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_latex
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_micro_essay
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_nuclei_template_rule
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── write_pull-request
│ │ │ │ │ │ └── system.md
│ │ │ │ │ └── write_semgrep_rule
│ │ │ │ │ ├── system.md
│ │ │ │ │ └── user.md
│ │ │ │ └── routines
│ │ │ │ ├── list.py
│ │ │ │ ├── run.py
│ │ │ │ └── show.py
│ │ │ ├── guided_conversation
│ │ │ │ ├── __init__.py
│ │ │ │ ├── agenda.py
│ │ │ │ ├── artifact_helpers.py
│ │ │ │ ├── chat_completions
│ │ │ │ │ ├── fix_agenda_error.py
│ │ │ │ │ ├── fix_artifact_error.py
│ │ │ │ │ ├── generate_agenda.py
│ │ │ │ │ ├── generate_artifact_updates.py
│ │ │ │ │ ├── generate_final_artifact.py
│ │ │ │ │ └── generate_message.py
│ │ │ │ ├── conversation_guides
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── acrostic_poem.py
│ │ │ │ │ ├── er_triage.py
│ │ │ │ │ ├── interview.py
│ │ │ │ │ └── patient_intake.py
│ │ │ │ ├── guide.py
│ │ │ │ ├── guided_conversation_skill.py
│ │ │ │ ├── logging.py
│ │ │ │ ├── message.py
│ │ │ │ ├── resources.py
│ │ │ │ ├── routines
│ │ │ │ │ └── guided_conversation.py
│ │ │ │ └── tests
│ │ │ │ ├── conftest.py
│ │ │ │ ├── test_artifact_helpers.py
│ │ │ │ ├── test_generate_agenda.py
│ │ │ │ ├── test_generate_artifact_updates.py
│ │ │ │ ├── test_generate_final_artifact.py
│ │ │ │ └── test_resource.py
│ │ │ ├── meta
│ │ │ │ ├── __init__.py
│ │ │ │ ├── meta_skill.py
│ │ │ │ ├── README.md
│ │ │ │ └── routines
│ │ │ │ └── generate_routine.py
│ │ │ ├── posix
│ │ │ │ ├── __init__.py
│ │ │ │ ├── posix_skill.py
│ │ │ │ ├── routines
│ │ │ │ │ ├── append_file.py
│ │ │ │ │ ├── cd.py
│ │ │ │ │ ├── ls.py
│ │ │ │ │ ├── make_home_dir.py
│ │ │ │ │ ├── mkdir.py
│ │ │ │ │ ├── mv.py
│ │ │ │ │ ├── pwd.py
│ │ │ │ │ ├── read_file.py
│ │ │ │ │ ├── rm.py
│ │ │ │ │ ├── touch.py
│ │ │ │ │ └── write_file.py
│ │ │ │ └── sandbox_shell.py
│ │ │ ├── README.md
│ │ │ ├── research
│ │ │ │ ├── __init__.py
│ │ │ │ ├── README.md
│ │ │ │ ├── research_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── answer_question_about_content.py
│ │ │ │ ├── evaluate_answer.py
│ │ │ │ ├── generate_research_plan.py
│ │ │ │ ├── generate_search_query.py
│ │ │ │ ├── update_research_plan.py
│ │ │ │ ├── web_research.py
│ │ │ │ └── web_search.py
│ │ │ ├── research2
│ │ │ │ ├── __init__.py
│ │ │ │ ├── README.md
│ │ │ │ ├── research_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── facts.py
│ │ │ │ ├── make_final_report.py
│ │ │ │ ├── research.py
│ │ │ │ ├── search_plan.py
│ │ │ │ ├── search.py
│ │ │ │ └── visit_pages.py
│ │ │ └── web_research
│ │ │ ├── __init__.py
│ │ │ ├── README.md
│ │ │ ├── research_skill.py
│ │ │ └── routines
│ │ │ ├── facts.py
│ │ │ ├── make_final_report.py
│ │ │ ├── research.py
│ │ │ ├── search_plan.py
│ │ │ ├── search.py
│ │ │ └── visit_pages.py
│ │ ├── tests
│ │ │ ├── test_common_skill.py
│ │ │ ├── test_integration.py
│ │ │ ├── test_routine_stack.py
│ │ │ ├── tst_skill
│ │ │ │ ├── __init__.py
│ │ │ │ └── routines
│ │ │ │ ├── __init__.py
│ │ │ │ └── a_routine.py
│ │ │ └── utilities
│ │ │ ├── test_find_template_vars.py
│ │ │ ├── test_make_arg_set.py
│ │ │ ├── test_paramspec.py
│ │ │ ├── test_parse_command_string.py
│ │ │ └── test_to_string.py
│ │ ├── types.py
│ │ ├── usage.py
│ │ └── utilities.py
│ └── uv.lock
├── LICENSE
├── Makefile
├── mcp-servers
│ ├── ai-assist-content
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── mcp-example-brave-search.md
│ │ ├── mcp-fastmcp-typescript-README.md
│ │ ├── mcp-llms-full.txt
│ │ ├── mcp-metadata-tips.md
│ │ ├── mcp-python-sdk-README.md
│ │ ├── mcp-typescript-sdk-README.md
│ │ ├── pydanticai-documentation.md
│ │ ├── pydanticai-example-question-graph.md
│ │ ├── pydanticai-example-weather.md
│ │ ├── pydanticai-tutorial.md
│ │ └── README.md
│ ├── Makefile
│ ├── mcp-server-bing-search
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_bing_search
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── clean_website.py
│ │ │ │ └── filter_links.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── tools.py
│ │ │ ├── types.py
│ │ │ ├── utils.py
│ │ │ └── web
│ │ │ ├── __init__.py
│ │ │ ├── get_content.py
│ │ │ ├── llm_processing.py
│ │ │ ├── process_website.py
│ │ │ └── search_bing.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_tools.py
│ │ └── uv.lock
│ ├── mcp-server-bundle
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_bundle
│ │ │ ├── __init__.py
│ │ │ └── main.py
│ │ ├── pyinstaller.spec
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-filesystem
│ │ ├── .env.example
│ │ ├── .github
│ │ │ └── workflows
│ │ │ └── ci.yml
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_filesystem
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_filesystem.py
│ │ └── uv.lock
│ ├── mcp-server-filesystem-edit
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── data
│ │ │ ├── attachments
│ │ │ │ ├── Daily Game Ideas.txt
│ │ │ │ ├── Frontend Framework Proposal.txt
│ │ │ │ ├── ReDoodle.txt
│ │ │ │ └── Research Template.tex
│ │ │ ├── test_cases.yaml
│ │ │ └── transcripts
│ │ │ ├── transcript_research_simple.md
│ │ │ ├── transcript_Startup_Idea_1_202503031513.md
│ │ │ ├── transcript_Startup_Idea_2_202503031659.md
│ │ │ └── transcript_Web_Frontends_202502281551.md
│ │ ├── Makefile
│ │ ├── mcp_server_filesystem_edit
│ │ │ ├── __init__.py
│ │ │ ├── app_handling
│ │ │ │ ├── __init__.py
│ │ │ │ ├── excel.py
│ │ │ │ ├── miktex.py
│ │ │ │ ├── office_common.py
│ │ │ │ ├── powerpoint.py
│ │ │ │ └── word.py
│ │ │ ├── config.py
│ │ │ ├── evals
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common.py
│ │ │ │ ├── run_comments.py
│ │ │ │ ├── run_edit.py
│ │ │ │ └── run_ppt_edit.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── add_comments.py
│ │ │ │ ├── analyze_comments.py
│ │ │ │ ├── latex_edit.py
│ │ │ │ ├── markdown_draft.py
│ │ │ │ ├── markdown_edit.py
│ │ │ │ └── powerpoint_edit.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── add_comments.py
│ │ │ │ ├── edit_adapters
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── common.py
│ │ │ │ │ ├── latex.py
│ │ │ │ │ └── markdown.py
│ │ │ │ ├── edit.py
│ │ │ │ └── helpers.py
│ │ │ └── types.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── app_handling
│ │ │ │ ├── test_excel.py
│ │ │ │ ├── test_miktext.py
│ │ │ │ ├── test_office_common.py
│ │ │ │ ├── test_powerpoint.py
│ │ │ │ └── test_word.py
│ │ │ ├── conftest.py
│ │ │ └── tools
│ │ │ └── edit_adapters
│ │ │ ├── test_latex.py
│ │ │ └── test_markdown.py
│ │ └── uv.lock
│ ├── mcp-server-fusion
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── AddInIcon.svg
│ │ ├── config.py
│ │ ├── FusionMCPServerAddIn.manifest
│ │ ├── FusionMCPServerAddIn.py
│ │ ├── mcp_server_fusion
│ │ │ ├── __init__.py
│ │ │ ├── fusion_mcp_server.py
│ │ │ ├── fusion_utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── event_utils.py
│ │ │ │ ├── general_utils.py
│ │ │ │ └── tool_utils.py
│ │ │ ├── mcp_tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── fusion_3d_operation.py
│ │ │ │ ├── fusion_geometry.py
│ │ │ │ ├── fusion_pattern.py
│ │ │ │ └── fusion_sketch.py
│ │ │ └── vendor
│ │ │ └── README.md
│ │ ├── README.md
│ │ └── requirements.txt
│ ├── mcp-server-giphy
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── giphy_search.py
│ │ │ ├── sampling.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ └── utils.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-memory-user-bio
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_memory_user_bio
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-memory-whiteboard
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_memory_whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-office
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── build.sh
│ │ ├── data
│ │ │ ├── attachments
│ │ │ │ ├── Daily Game Ideas.txt
│ │ │ │ ├── Frontend Framework Proposal.txt
│ │ │ │ └── ReDoodle.txt
│ │ │ └── word
│ │ │ ├── test_cases.yaml
│ │ │ └── transcripts
│ │ │ ├── transcript_Startup_Idea_1_202503031513.md
│ │ │ ├── transcript_Startup_Idea_2_202503031659.md
│ │ │ └── transcript_Web_Frontends_202502281551.md
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── app_interaction
│ │ │ │ ├── __init__.py
│ │ │ │ ├── excel_editor.py
│ │ │ │ ├── powerpoint_editor.py
│ │ │ │ └── word_editor.py
│ │ │ ├── config.py
│ │ │ ├── constants.py
│ │ │ ├── evals
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common.py
│ │ │ │ ├── run_comment_analysis.py
│ │ │ │ ├── run_feedback.py
│ │ │ │ └── run_markdown_edit.py
│ │ │ ├── helpers.py
│ │ │ ├── markdown_edit
│ │ │ │ ├── __init__.py
│ │ │ │ ├── comment_analysis.py
│ │ │ │ ├── feedback_step.py
│ │ │ │ ├── markdown_edit.py
│ │ │ │ └── utils.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── comment_analysis.py
│ │ │ │ ├── feedback.py
│ │ │ │ ├── markdown_draft.py
│ │ │ │ └── markdown_edit.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ └── types.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_word_editor.py
│ │ └── uv.lock
│ ├── mcp-server-open-deep-research
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── libs
│ │ │ │ └── open_deep_research
│ │ │ │ ├── cookies.py
│ │ │ │ ├── mdconvert.py
│ │ │ │ ├── run_agents.py
│ │ │ │ ├── text_inspector_tool.py
│ │ │ │ ├── text_web_browser.py
│ │ │ │ └── visual_qa.py
│ │ │ ├── open_deep_research.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-open-deep-research-clone
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_open_deep_research_clone
│ │ │ ├── __init__.py
│ │ │ ├── azure_openai.py
│ │ │ ├── config.py
│ │ │ ├── logging.py
│ │ │ ├── sampling.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── utils.py
│ │ │ └── web_research.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ └── test_open_deep_research_clone.py
│ │ └── uv.lock
│ ├── mcp-server-template
│ │ ├── .taplo.toml
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── copier.yml
│ │ ├── README.md
│ │ └── template
│ │ └── {{ project_slug }}
│ │ ├── .env.example.jinja
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json.jinja
│ │ │ └── settings.json
│ │ ├── {{ module_name }}
│ │ │ ├── __init__.py
│ │ │ ├── config.py.jinja
│ │ │ ├── server.py.jinja
│ │ │ └── start.py.jinja
│ │ ├── Makefile.jinja
│ │ ├── pyproject.toml.jinja
│ │ └── README.md.jinja
│ ├── mcp-server-vscode
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── .npmrc
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ ├── launch.json
│ │ │ ├── settings.json
│ │ │ └── tasks.json
│ │ ├── .vscode-test.mjs
│ │ ├── .vscodeignore
│ │ ├── ASSISTANT_BOOTSTRAP.md
│ │ ├── eslint.config.mjs
│ │ ├── images
│ │ │ └── icon.png
│ │ ├── LICENSE
│ │ ├── Makefile
│ │ ├── out
│ │ │ ├── extension.d.ts
│ │ │ ├── extension.js
│ │ │ ├── test
│ │ │ │ ├── extension.test.d.ts
│ │ │ │ └── extension.test.js
│ │ │ ├── tools
│ │ │ │ ├── code_checker.d.ts
│ │ │ │ ├── code_checker.js
│ │ │ │ ├── debug_tools.d.ts
│ │ │ │ ├── debug_tools.js
│ │ │ │ ├── focus_editor.d.ts
│ │ │ │ ├── focus_editor.js
│ │ │ │ ├── search_symbol.d.ts
│ │ │ │ └── search_symbol.js
│ │ │ └── utils
│ │ │ ├── port.d.ts
│ │ │ └── port.js
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── prettier.config.cjs
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── extension.d.ts
│ │ │ ├── extension.ts
│ │ │ ├── test
│ │ │ │ ├── extension.test.d.ts
│ │ │ │ └── extension.test.ts
│ │ │ ├── tools
│ │ │ │ ├── code_checker.d.ts
│ │ │ │ ├── code_checker.ts
│ │ │ │ ├── debug_tools.d.ts
│ │ │ │ ├── debug_tools.ts
│ │ │ │ ├── focus_editor.d.ts
│ │ │ │ ├── focus_editor.ts
│ │ │ │ ├── search_symbol.d.ts
│ │ │ │ └── search_symbol.ts
│ │ │ └── utils
│ │ │ ├── port.d.ts
│ │ │ └── port.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.tsbuildinfo
│ │ ├── vsc-extension-quickstart.md
│ │ └── webpack.config.js
│ └── mcp-server-web-research
│ ├── .env.example
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── Makefile
│ ├── mcp_server_web_research
│ │ ├── __init__.py
│ │ ├── azure_openai.py
│ │ ├── config.py
│ │ ├── logging.py
│ │ ├── sampling.py
│ │ ├── server.py
│ │ ├── start.py
│ │ ├── utils.py
│ │ └── web_research.py
│ ├── pyproject.toml
│ ├── README.md
│ ├── test
│ │ └── test_web_research.py
│ └── uv.lock
├── README.md
├── RESPONSIBLE_AI_FAQ.md
├── ruff.toml
├── SECURITY.md
├── semantic-workbench.code-workspace
├── SUPPORT.md
├── tools
│ ├── build_ai_context_files.py
│ ├── collect_files.py
│ ├── docker
│ │ ├── azure_website_sshd.conf
│ │ ├── docker-entrypoint.sh
│ │ ├── Dockerfile.assistant
│ │ └── Dockerfile.mcp-server
│ ├── makefiles
│ │ ├── docker-assistant.mk
│ │ ├── docker-mcp-server.mk
│ │ ├── docker.mk
│ │ ├── python.mk
│ │ ├── recursive.mk
│ │ └── shell.mk
│ ├── reset-service-data.ps1
│ ├── reset-service-data.sh
│ ├── run-app.ps1
│ ├── run-app.sh
│ ├── run-canonical-agent.ps1
│ ├── run-canonical-agent.sh
│ ├── run-dotnet-examples-with-aspire.sh
│ ├── run-python-example1.sh
│ ├── run-python-example2.ps1
│ ├── run-python-example2.sh
│ ├── run-service.ps1
│ ├── run-service.sh
│ ├── run-workbench-chatbot.ps1
│ └── run-workbench-chatbot.sh
├── workbench-app
│ ├── .dockerignore
│ ├── .env.example
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── docker-entrypoint.sh
│ ├── Dockerfile
│ ├── docs
│ │ ├── APP_DEV_GUIDE.md
│ │ ├── MESSAGE_METADATA.md
│ │ ├── MESSAGE_TYPES.md
│ │ ├── README.md
│ │ └── STATE_INSPECTORS.md
│ ├── index.html
│ ├── Makefile
│ ├── nginx.conf
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── prettier.config.cjs
│ ├── public
│ │ └── assets
│ │ ├── background-1-upscaled.jpg
│ │ ├── background-1-upscaled.png
│ │ ├── background-1.jpg
│ │ ├── background-1.png
│ │ ├── background-2.jpg
│ │ ├── background-2.png
│ │ ├── experimental-feature.jpg
│ │ ├── favicon.svg
│ │ ├── workflow-designer-1.jpg
│ │ ├── workflow-designer-outlets.jpg
│ │ ├── workflow-designer-states.jpg
│ │ └── workflow-designer-transitions.jpg
│ ├── README.md
│ ├── run.sh
│ ├── src
│ │ ├── components
│ │ │ ├── App
│ │ │ │ ├── AppFooter.tsx
│ │ │ │ ├── AppHeader.tsx
│ │ │ │ ├── AppMenu.tsx
│ │ │ │ ├── AppView.tsx
│ │ │ │ ├── CodeLabel.tsx
│ │ │ │ ├── CommandButton.tsx
│ │ │ │ ├── ConfirmLeave.tsx
│ │ │ │ ├── ContentExport.tsx
│ │ │ │ ├── ContentImport.tsx
│ │ │ │ ├── CopyButton.tsx
│ │ │ │ ├── DialogControl.tsx
│ │ │ │ ├── DynamicIframe.tsx
│ │ │ │ ├── ErrorListFromAppState.tsx
│ │ │ │ ├── ErrorMessageBar.tsx
│ │ │ │ ├── ExperimentalNotice.tsx
│ │ │ │ ├── FormWidgets
│ │ │ │ │ ├── BaseModelEditorWidget.tsx
│ │ │ │ │ ├── CustomizedArrayFieldTemplate.tsx
│ │ │ │ │ ├── CustomizedFieldTemplate.tsx
│ │ │ │ │ ├── CustomizedObjectFieldTemplate.tsx
│ │ │ │ │ └── InspectableWidget.tsx
│ │ │ │ ├── LabelWithDescription.tsx
│ │ │ │ ├── Loading.tsx
│ │ │ │ ├── MenuItemControl.tsx
│ │ │ │ ├── MiniControl.tsx
│ │ │ │ ├── MyAssistantServiceRegistrations.tsx
│ │ │ │ ├── MyItemsManager.tsx
│ │ │ │ ├── OverflowMenu.tsx
│ │ │ │ ├── PresenceMotionList.tsx
│ │ │ │ ├── ProfileSettings.tsx
│ │ │ │ └── TooltipWrapper.tsx
│ │ │ ├── Assistants
│ │ │ │ ├── ApplyConfigButton.tsx
│ │ │ │ ├── AssistantAdd.tsx
│ │ │ │ ├── AssistantConfigExportButton.tsx
│ │ │ │ ├── AssistantConfigImportButton.tsx
│ │ │ │ ├── AssistantConfiguration.tsx
│ │ │ │ ├── AssistantConfigure.tsx
│ │ │ │ ├── AssistantCreate.tsx
│ │ │ │ ├── AssistantDelete.tsx
│ │ │ │ ├── AssistantDuplicate.tsx
│ │ │ │ ├── AssistantExport.tsx
│ │ │ │ ├── AssistantImport.tsx
│ │ │ │ ├── AssistantRemove.tsx
│ │ │ │ ├── AssistantRename.tsx
│ │ │ │ ├── AssistantServiceInfo.tsx
│ │ │ │ ├── AssistantServiceMetadata.tsx
│ │ │ │ └── MyAssistants.tsx
│ │ │ ├── AssistantServiceRegistrations
│ │ │ │ ├── AssistantServiceRegistrationApiKey.tsx
│ │ │ │ ├── AssistantServiceRegistrationApiKeyReset.tsx
│ │ │ │ ├── AssistantServiceRegistrationCreate.tsx
│ │ │ │ └── AssistantServiceRegistrationRemove.tsx
│ │ │ ├── Conversations
│ │ │ │ ├── Canvas
│ │ │ │ │ ├── AssistantCanvas.tsx
│ │ │ │ │ ├── AssistantCanvasList.tsx
│ │ │ │ │ ├── AssistantInspector.tsx
│ │ │ │ │ ├── AssistantInspectorList.tsx
│ │ │ │ │ └── ConversationCanvas.tsx
│ │ │ │ ├── ChatInputPlugins
│ │ │ │ │ ├── ClearEditorPlugin.tsx
│ │ │ │ │ ├── LexicalMenu.ts
│ │ │ │ │ ├── ParticipantMentionsPlugin.tsx
│ │ │ │ │ ├── TypeaheadMenuPlugin.css
│ │ │ │ │ └── TypeaheadMenuPlugin.tsx
│ │ │ │ ├── ContentRenderers
│ │ │ │ │ ├── CodeContentRenderer.tsx
│ │ │ │ │ ├── ContentListRenderer.tsx
│ │ │ │ │ ├── ContentRenderer.tsx
│ │ │ │ │ ├── DiffRenderer.tsx
│ │ │ │ │ ├── HtmlContentRenderer.tsx
│ │ │ │ │ ├── JsonSchemaContentRenderer.tsx
│ │ │ │ │ ├── MarkdownContentRenderer.tsx
│ │ │ │ │ ├── MarkdownEditorRenderer.tsx
│ │ │ │ │ ├── MermaidContentRenderer.tsx
│ │ │ │ │ ├── MusicABCContentRenderer.css
│ │ │ │ │ └── MusicABCContentRenderer.tsx
│ │ │ │ ├── ContextWindow.tsx
│ │ │ │ ├── ConversationCreate.tsx
│ │ │ │ ├── ConversationDuplicate.tsx
│ │ │ │ ├── ConversationExport.tsx
│ │ │ │ ├── ConversationFileIcon.tsx
│ │ │ │ ├── ConversationRemove.tsx
│ │ │ │ ├── ConversationRename.tsx
│ │ │ │ ├── ConversationShare.tsx
│ │ │ │ ├── ConversationShareCreate.tsx
│ │ │ │ ├── ConversationShareList.tsx
│ │ │ │ ├── ConversationShareView.tsx
│ │ │ │ ├── ConversationsImport.tsx
│ │ │ │ ├── ConversationTranscript.tsx
│ │ │ │ ├── DebugInspector.tsx
│ │ │ │ ├── FileItem.tsx
│ │ │ │ ├── FileList.tsx
│ │ │ │ ├── InputAttachmentList.tsx
│ │ │ │ ├── InputOptionsControl.tsx
│ │ │ │ ├── InteractHistory.tsx
│ │ │ │ ├── InteractInput.tsx
│ │ │ │ ├── Message
│ │ │ │ │ ├── AttachmentSection.tsx
│ │ │ │ │ ├── ContentRenderer.tsx
│ │ │ │ │ ├── ContentSafetyNotice.tsx
│ │ │ │ │ ├── InteractMessage.tsx
│ │ │ │ │ ├── MessageActions.tsx
│ │ │ │ │ ├── MessageBase.tsx
│ │ │ │ │ ├── MessageBody.tsx
│ │ │ │ │ ├── MessageContent.tsx
│ │ │ │ │ ├── MessageFooter.tsx
│ │ │ │ │ ├── MessageHeader.tsx
│ │ │ │ │ ├── NotificationAccordion.tsx
│ │ │ │ │ └── ToolResultMessage.tsx
│ │ │ │ ├── MessageDelete.tsx
│ │ │ │ ├── MessageLink.tsx
│ │ │ │ ├── MyConversations.tsx
│ │ │ │ ├── MyShares.tsx
│ │ │ │ ├── ParticipantAvatar.tsx
│ │ │ │ ├── ParticipantAvatarGroup.tsx
│ │ │ │ ├── ParticipantItem.tsx
│ │ │ │ ├── ParticipantList.tsx
│ │ │ │ ├── ParticipantStatus.tsx
│ │ │ │ ├── RewindConversation.tsx
│ │ │ │ ├── ShareRemove.tsx
│ │ │ │ ├── SpeechButton.tsx
│ │ │ │ └── ToolCalls.tsx
│ │ │ └── FrontDoor
│ │ │ ├── Chat
│ │ │ │ ├── AssistantDrawer.tsx
│ │ │ │ ├── CanvasDrawer.tsx
│ │ │ │ ├── Chat.tsx
│ │ │ │ ├── ChatCanvas.tsx
│ │ │ │ ├── ChatControls.tsx
│ │ │ │ └── ConversationDrawer.tsx
│ │ │ ├── Controls
│ │ │ │ ├── AssistantCard.tsx
│ │ │ │ ├── AssistantSelector.tsx
│ │ │ │ ├── AssistantServiceSelector.tsx
│ │ │ │ ├── ConversationItem.tsx
│ │ │ │ ├── ConversationList.tsx
│ │ │ │ ├── ConversationListOptions.tsx
│ │ │ │ ├── NewConversationButton.tsx
│ │ │ │ ├── NewConversationForm.tsx
│ │ │ │ └── SiteMenuButton.tsx
│ │ │ ├── GlobalContent.tsx
│ │ │ └── MainContent.tsx
│ │ ├── Constants.ts
│ │ ├── global.d.ts
│ │ ├── index.css
│ │ ├── libs
│ │ │ ├── AppStorage.ts
│ │ │ ├── AuthHelper.ts
│ │ │ ├── EventSubscriptionManager.ts
│ │ │ ├── Theme.ts
│ │ │ ├── useAssistantCapabilities.ts
│ │ │ ├── useChatCanvasController.ts
│ │ │ ├── useConversationEvents.ts
│ │ │ ├── useConversationUtility.ts
│ │ │ ├── useCreateConversation.ts
│ │ │ ├── useDebugComponentLifecycle.ts
│ │ │ ├── useDragAndDrop.ts
│ │ │ ├── useEnvironment.ts
│ │ │ ├── useExportUtility.ts
│ │ │ ├── useHistoryUtility.ts
│ │ │ ├── useKeySequence.ts
│ │ │ ├── useMediaQuery.ts
│ │ │ ├── useMicrosoftGraph.ts
│ │ │ ├── useNotify.tsx
│ │ │ ├── useParticipantUtility.tsx
│ │ │ ├── useSiteUtility.ts
│ │ │ ├── useWorkbenchEventSource.ts
│ │ │ ├── useWorkbenchService.ts
│ │ │ └── Utility.ts
│ │ ├── main.tsx
│ │ ├── models
│ │ │ ├── Assistant.ts
│ │ │ ├── AssistantCapability.ts
│ │ │ ├── AssistantServiceInfo.ts
│ │ │ ├── AssistantServiceRegistration.ts
│ │ │ ├── Config.ts
│ │ │ ├── Conversation.ts
│ │ │ ├── ConversationFile.ts
│ │ │ ├── ConversationMessage.ts
│ │ │ ├── ConversationMessageDebug.ts
│ │ │ ├── ConversationParticipant.ts
│ │ │ ├── ConversationShare.ts
│ │ │ ├── ConversationShareRedemption.ts
│ │ │ ├── ConversationState.ts
│ │ │ ├── ConversationStateDescription.ts
│ │ │ ├── ServiceEnvironment.ts
│ │ │ └── User.ts
│ │ ├── redux
│ │ │ ├── app
│ │ │ │ ├── hooks.ts
│ │ │ │ ├── rtkQueryErrorLogger.ts
│ │ │ │ └── store.ts
│ │ │ └── features
│ │ │ ├── app
│ │ │ │ ├── appSlice.ts
│ │ │ │ └── AppState.ts
│ │ │ ├── chatCanvas
│ │ │ │ ├── chatCanvasSlice.ts
│ │ │ │ └── ChatCanvasState.ts
│ │ │ ├── localUser
│ │ │ │ ├── localUserSlice.ts
│ │ │ │ └── LocalUserState.ts
│ │ │ └── settings
│ │ │ ├── settingsSlice.ts
│ │ │ └── SettingsState.ts
│ │ ├── Root.tsx
│ │ ├── routes
│ │ │ ├── AcceptTerms.tsx
│ │ │ ├── AssistantEditor.tsx
│ │ │ ├── AssistantServiceRegistrationEditor.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── ErrorPage.tsx
│ │ │ ├── FrontDoor.tsx
│ │ │ ├── Login.tsx
│ │ │ ├── Settings.tsx
│ │ │ ├── ShareRedeem.tsx
│ │ │ └── Shares.tsx
│ │ ├── services
│ │ │ └── workbench
│ │ │ ├── assistant.ts
│ │ │ ├── assistantService.ts
│ │ │ ├── conversation.ts
│ │ │ ├── file.ts
│ │ │ ├── index.ts
│ │ │ ├── participant.ts
│ │ │ ├── share.ts
│ │ │ ├── state.ts
│ │ │ └── workbench.ts
│ │ └── vite-env.d.ts
│ ├── tools
│ │ └── filtered-ts-prune.cjs
│ ├── tsconfig.json
│ └── vite.config.ts
└── workbench-service
├── .env.example
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── alembic.ini
├── devdb
│ ├── docker-compose.yaml
│ └── postgresql-init.sh
├── Dockerfile
├── Makefile
├── migrations
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions
│ ├── 2024_09_19_000000_69dcda481c14_init.py
│ ├── 2024_09_19_190029_dffb1d7e219a_file_version_filename.py
│ ├── 2024_09_20_204130_b29524775484_share.py
│ ├── 2024_10_30_231536_039bec8edc33_index_message_type.py
│ ├── 2024_11_04_204029_5149c7fb5a32_conversationmessagedebug.py
│ ├── 2024_11_05_015124_245baf258e11_double_check_debugs.py
│ ├── 2024_11_25_191056_a106de176394_drop_workflow.py
│ ├── 2025_03_19_140136_aaaf792d4d72_set_user_title_set.py
│ ├── 2025_03_21_153250_3763629295ad_add_assistant_template_id.py
│ ├── 2025_05_19_163613_b2f86e981885_delete_context_transfer_assistants.py
│ └── 2025_06_18_174328_503c739152f3_delete_knowlege_transfer_assistants.py
├── pyproject.toml
├── README.md
├── semantic_workbench_service
│ ├── __init__.py
│ ├── api.py
│ ├── assistant_api_key.py
│ ├── auth.py
│ ├── azure_speech.py
│ ├── config.py
│ ├── controller
│ │ ├── __init__.py
│ │ ├── assistant_service_client_pool.py
│ │ ├── assistant_service_registration.py
│ │ ├── assistant.py
│ │ ├── conversation_share.py
│ │ ├── conversation.py
│ │ ├── convert.py
│ │ ├── exceptions.py
│ │ ├── export_import.py
│ │ ├── file.py
│ │ ├── participant.py
│ │ └── user.py
│ ├── db.py
│ ├── event.py
│ ├── files.py
│ ├── logging_config.py
│ ├── middleware.py
│ ├── query.py
│ ├── service_user_principals.py
│ ├── service.py
│ └── start.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── docker-compose.yaml
│ ├── test_assistant_api_key.py
│ ├── test_files.py
│ ├── test_integration.py
│ ├── test_middleware.py
│ ├── test_migrations.py
│ ├── test_workbench_service.py
│ └── types.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/assistants/knowledge-transfer-assistant/assistant/assistant.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | # Project Assistant implementation
4 |
5 | import asyncio
6 | import pathlib
7 | from typing import Any
8 |
9 | from assistant_extensions import attachments, dashboard_card, navigator
10 | from content_safety.evaluators import CombinedContentSafetyEvaluator
11 | from semantic_workbench_api_model import workbench_model
12 | from semantic_workbench_api_model.workbench_model import (
13 | AssistantStateEvent,
14 | ConversationEvent,
15 | ConversationMessage,
16 | MessageType,
17 | NewConversationMessage,
18 | ParticipantRole,
19 | UpdateParticipant,
20 | )
21 | from semantic_workbench_assistant.assistant_app import (
22 | AssistantApp,
23 | AssistantCapability,
24 | ContentSafety,
25 | ContentSafetyEvaluator,
26 | ConversationContext,
27 | )
28 |
29 | from .agentic.team_welcome import generate_team_welcome_message
30 | from .common import detect_assistant_role, detect_conversation_type, get_shared_conversation_id, ConversationType
31 | from .config import assistant_config
32 | from .conversation_share_link import ConversationKnowledgePackageManager
33 | from .data import InspectorTab, LogEntryType
34 | from .files import ShareFilesManager
35 | from .logging import logger
36 | from .domain import KnowledgeTransferManager
37 | from .notifications import Notifications
38 | from .respond import respond_to_conversation
39 | from .ui_tabs import BriefInspector, LearningInspector, SharingInspector, DebugInspector
40 | from .storage import ShareStorage
41 | from .storage_models import ConversationRole
42 | from .utils import (
43 | DEFAULT_TEMPLATE_ID,
44 | load_text_include,
45 | )
46 |
47 | service_id = "knowledge-transfer-assistant.made-exploration"
48 | service_name = "Knowledge Transfer Assistant"
49 | service_description = "A mediator assistant that facilitates sharing knowledge between parties."
50 |
51 |
52 | async def content_evaluator_factory(
53 | context: ConversationContext,
54 | ) -> ContentSafetyEvaluator:
55 | config = await assistant_config.get(context.assistant)
56 | return CombinedContentSafetyEvaluator(config.content_safety_config)
57 |
58 |
59 | content_safety = ContentSafety(content_evaluator_factory)
60 |
61 | assistant = AssistantApp(
62 | assistant_service_id=service_id,
63 | assistant_service_name=service_name,
64 | assistant_service_description=service_description,
65 | config_provider=assistant_config.provider,
66 | content_interceptor=content_safety,
67 | capabilities={AssistantCapability.supports_conversation_files},
68 | inspector_state_providers={
69 | InspectorTab.BRIEF: BriefInspector(assistant_config),
70 | InspectorTab.LEARNING: LearningInspector(assistant_config),
71 | InspectorTab.SHARING: SharingInspector(assistant_config),
72 | InspectorTab.DEBUG: DebugInspector(assistant_config),
73 | },
74 | assistant_service_metadata={
75 | **dashboard_card.metadata(
76 | dashboard_card.TemplateConfig(
77 | enabled=True,
78 | template_id=DEFAULT_TEMPLATE_ID,
79 | background_color="rgb(198, 177, 222)",
80 | icon=dashboard_card.image_to_url(
81 | pathlib.Path(__file__).parent / "assets" / "icon-knowledge-transfer.svg", "image/svg+xml"
82 | ),
83 | card_content=dashboard_card.CardContent(
84 | content_type="text/markdown",
85 | content=load_text_include("card_content.md"),
86 | ),
87 | ),
88 | ),
89 | **navigator.metadata_for_assistant_navigator({
90 | "default": load_text_include("assistant_info.md"),
91 | }),
92 | },
93 | )
94 |
95 | attachments_extension = attachments.AttachmentsExtension(assistant)
96 |
97 | app = assistant.fastapi_app()
98 |
99 |
100 | @assistant.events.conversation.on_created_including_mine
101 | async def on_conversation_created(context: ConversationContext) -> None:
102 | """
103 | The assistant manages three types of conversations:
104 | 1. Coordinator Conversation: The main conversation used by the knowledge coordinator
105 | 2. Shareable Team Conversation: A template conversation that has a share URL and is never directly used
106 | 3. Team Conversation(s): Individual conversations for team members created when they redeem the share URL
107 | """
108 |
109 | conversation = await context.get_conversation()
110 | conversation_metadata = conversation.metadata or {}
111 | share_id = conversation_metadata.get("share_id")
112 |
113 | config = await assistant_config.get(context.assistant)
114 | conversation_type = detect_conversation_type(conversation)
115 |
116 | match conversation_type:
117 | case ConversationType.SHAREABLE_TEMPLATE:
118 | # Associate the shareable template with a share ID
119 | if not share_id:
120 | logger.error("No share ID found for shareable team conversation.")
121 | return
122 | await ConversationKnowledgePackageManager.associate_conversation_with_share(context, share_id)
123 | return
124 |
125 | case ConversationType.TEAM:
126 | if not share_id:
127 | logger.error("No share ID found for team conversation.")
128 | return
129 |
130 | # I'd put status messages here, but the attachment's extension is causing race conditions.
131 | await context.send_messages(
132 | NewConversationMessage(
133 | content="Hold on a second while I set up your space...",
134 | message_type=MessageType.chat,
135 | )
136 | )
137 |
138 | await ConversationKnowledgePackageManager.associate_conversation_with_share(context, share_id)
139 | await ConversationKnowledgePackageManager.set_conversation_role(context, share_id, ConversationRole.TEAM)
140 | await ShareFilesManager.synchronize_files_to_team_conversation(context=context, share_id=share_id)
141 |
142 | welcome_message, debug = await generate_team_welcome_message(context)
143 | await context.send_messages(
144 | NewConversationMessage(
145 | content=welcome_message,
146 | message_type=MessageType.chat,
147 | metadata={
148 | "generated_content": True,
149 | "debug": debug,
150 | },
151 | )
152 | )
153 |
154 | # Pop open the inspector panel.
155 | await context.send_conversation_state_event(
156 | AssistantStateEvent(
157 | state_id="brief",
158 | event="focus",
159 | state=None,
160 | )
161 | )
162 |
163 | return
164 |
165 | case ConversationType.COORDINATOR:
166 | try:
167 | # In the beginning, we created a share...
168 | share_id = await KnowledgeTransferManager.create_share(context)
169 |
170 | # And it was good. So we then created a sharable conversation that we use as a template.
171 | share_url = await KnowledgeTransferManager.create_shareable_team_conversation(
172 | context=context, share_id=share_id
173 | )
174 |
175 | welcome_message = config.coordinator_config.welcome_message.format(
176 | share_url=share_url or "<Share URL generation failed>"
177 | )
178 |
179 | except Exception as e:
180 | welcome_message = f"I'm having trouble setting up your knowledge transfer. Please try again or contact support if the issue persists. {str(e)}"
181 |
182 | await context.send_messages(
183 | NewConversationMessage(
184 | content=welcome_message,
185 | message_type=MessageType.chat,
186 | )
187 | )
188 |
189 | # Pop open the inspector panel.
190 | await context.send_conversation_state_event(
191 | AssistantStateEvent(
192 | state_id="brief",
193 | event="focus",
194 | state=None,
195 | )
196 | )
197 |
198 |
199 | @assistant.events.conversation.on_updated
200 | async def on_conversation_updated(context: ConversationContext) -> None:
201 | """
202 | Handle conversation updates (including title changes) and sync with shareable template.
203 | """
204 | try:
205 | conversation = await context.get_conversation()
206 | conversation_type = detect_conversation_type(conversation)
207 | if conversation_type != ConversationType.COORDINATOR:
208 | return
209 |
210 | shared_conversation_id = await get_shared_conversation_id(context)
211 | if not shared_conversation_id:
212 | return
213 |
214 | # Update the shareable template conversation's title if needed.
215 | try:
216 | target_context = context.for_conversation(shared_conversation_id)
217 | target_conversation = await target_context.get_conversation()
218 | if target_conversation.title != conversation.title:
219 | await target_context.update_conversation_title(conversation.title)
220 | logger.debug(
221 | f"Updated conversation {shared_conversation_id} title from '{target_conversation.title}' to '{conversation.title}'"
222 | )
223 | else:
224 | logger.debug(f"Conversation {shared_conversation_id} title already matches: '{conversation.title}'")
225 | except Exception as title_update_error:
226 | logger.error(f"Error updating conversation {shared_conversation_id} title: {title_update_error}")
227 |
228 | except Exception as e:
229 | logger.error(f"Error syncing conversation title: {e}")
230 |
231 |
232 | @assistant.events.conversation.message.chat.on_created
233 | async def on_message_created(
234 | context: ConversationContext, event: ConversationEvent, message: ConversationMessage
235 | ) -> None:
236 | await context.update_participant_me(UpdateParticipant(status="thinking..."))
237 |
238 | metadata: dict[str, Any] = {
239 | "debug": {
240 | "content_safety": event.data.get(content_safety.metadata_key, {}),
241 | }
242 | }
243 |
244 | try:
245 | share_id = await KnowledgeTransferManager.get_share_id(context)
246 | metadata["debug"]["share_id"] = share_id
247 |
248 | # If this is a Coordinator conversation, store the message for Team access
249 | async with context.set_status("jotting..."):
250 | role = await detect_assistant_role(context)
251 | if role == ConversationRole.COORDINATOR and message.message_type == MessageType.chat:
252 | try:
253 | if share_id:
254 | # Get the sender's name
255 | sender_name = "Coordinator"
256 | if message.sender:
257 | participants = await context.get_participants()
258 | for participant in participants.participants:
259 | if participant.id == message.sender.participant_id:
260 | sender_name = participant.name
261 | break
262 |
263 | # Store the message for Team access
264 | ShareStorage.append_coordinator_message(
265 | share_id=share_id,
266 | message_id=str(message.id),
267 | content=message.content,
268 | sender_name=sender_name,
269 | is_assistant=message.sender.participant_role == ParticipantRole.assistant,
270 | timestamp=message.timestamp,
271 | )
272 |
273 | # If this is the coordinator's first message, pop the share canvas
274 | messages = await context.get_messages()
275 | if len(messages.messages) == 2:
276 | await context.send_conversation_state_event(
277 | AssistantStateEvent(
278 | state_id="brief",
279 | event="focus",
280 | state=None,
281 | )
282 | )
283 | except Exception as e:
284 | # Don't fail message handling if storage fails
285 | logger.exception(f"Error storing Coordinator message for Team access: {e}")
286 |
287 | async with context.set_status("pondering..."):
288 | await respond_to_conversation(
289 | context,
290 | new_message=message,
291 | attachments_extension=attachments_extension,
292 | metadata=metadata,
293 | )
294 |
295 | # If the message is from a Coordinator, update the whiteboard in the background
296 | if role == ConversationRole.COORDINATOR and message.message_type == MessageType.chat:
297 | asyncio.create_task(KnowledgeTransferManager.auto_update_knowledge_digest(context))
298 |
299 | except Exception as e:
300 | logger.exception(f"Error handling message: {e}")
301 | await context.send_messages(
302 | NewConversationMessage(
303 | content=f"Error: {str(e)}",
304 | message_type=MessageType.notice,
305 | metadata={"generated_content": False, **metadata},
306 | )
307 | )
308 | finally:
309 | await context.update_participant_me(UpdateParticipant(status=None))
310 |
311 |
312 | @assistant.events.conversation.message.command.on_created
313 | async def on_command_created(
314 | context: ConversationContext, event: ConversationEvent, message: ConversationMessage
315 | ) -> None:
316 | if message.message_type != MessageType.command:
317 | return
318 |
319 | await context.update_participant_me(UpdateParticipant(status="processing command..."))
320 | try:
321 | metadata = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}}
322 |
323 | # Respond to the conversation
324 | await respond_to_conversation(
325 | context,
326 | new_message=message,
327 | attachments_extension=attachments_extension,
328 | metadata=metadata,
329 | )
330 | finally:
331 | # update the participant status to indicate the assistant is done thinking
332 | await context.update_participant_me(UpdateParticipant(status=None))
333 |
334 |
335 | @assistant.events.conversation.file.on_created
336 | async def on_file_created(
337 | context: ConversationContext,
338 | event: workbench_model.ConversationEvent,
339 | file: workbench_model.File,
340 | ) -> None:
341 | """
342 | Handle when a file is created in the conversation.
343 |
344 | For Coordinator files:
345 | 1. Store a copy in share storage
346 | 2. Synchronize to all Team conversations
347 |
348 | For Team files:
349 | 1. Use as-is without copying to share storage
350 | """
351 | try:
352 | share_id = await KnowledgeTransferManager.get_share_id(context)
353 | if not share_id or not file.filename:
354 | logger.warning(f"No share ID found or missing filename: share_id={share_id}, filename={file.filename}")
355 | return
356 |
357 | role = await detect_assistant_role(context)
358 |
359 | # Process based on role
360 | if role == ConversationRole.COORDINATOR:
361 | # For Coordinator files:
362 | # 1. Store in share storage (marked as coordinator file)
363 |
364 | success = await ShareFilesManager.copy_file_to_share_storage(
365 | context=context,
366 | share_id=share_id,
367 | file=file,
368 | is_coordinator_file=True,
369 | )
370 |
371 | if not success:
372 | logger.error(f"Failed to copy file to share storage: {file.filename}")
373 | return
374 |
375 | # 2. Synchronize to all Team conversations
376 | # Get all Team conversations
377 | team_conversations = await ShareFilesManager.get_team_conversations(context, share_id)
378 |
379 | if team_conversations:
380 | for team_conv_id in team_conversations:
381 | await ShareFilesManager.copy_file_to_conversation(
382 | context=context,
383 | share_id=share_id,
384 | filename=file.filename,
385 | target_conversation_id=team_conv_id,
386 | )
387 |
388 | # 3. Update all UIs but don't send notifications to reduce noise
389 | await Notifications.notify_all_state_update(context, share_id, [InspectorTab.DEBUG])
390 | # Team files don't need special handling as they're already in the conversation
391 |
392 | # Log file creation to knowledge transfer log for all files
393 | await ShareStorage.log_share_event(
394 | context=context,
395 | share_id=share_id,
396 | entry_type="file_shared",
397 | message=f"File shared: {file.filename}",
398 | metadata={
399 | "file_id": getattr(file, "id", ""),
400 | "filename": file.filename,
401 | "is_coordinator_file": role.value == "coordinator",
402 | },
403 | )
404 |
405 | except Exception as e:
406 | logger.exception(f"Error handling file creation: {e}")
407 |
408 |
409 | @assistant.events.conversation.file.on_updated
410 | async def on_file_updated(
411 | context: ConversationContext,
412 | event: workbench_model.ConversationEvent,
413 | file: workbench_model.File,
414 | ) -> None:
415 | try:
416 | # Get share ID
417 | share_id = await KnowledgeTransferManager.get_share_id(context)
418 | if not share_id or not file.filename:
419 | return
420 |
421 | role = await detect_assistant_role(context)
422 | if role == ConversationRole.COORDINATOR:
423 | # For Coordinator files:
424 | # 1. Update in share storage
425 | success = await ShareFilesManager.copy_file_to_share_storage(
426 | context=context,
427 | share_id=share_id,
428 | file=file,
429 | is_coordinator_file=True,
430 | )
431 |
432 | if not success:
433 | logger.error(f"Failed to update file in share storage: {file.filename}")
434 | return
435 |
436 | team_conversations = await ShareFilesManager.get_team_conversations(context, share_id)
437 | for team_conv_id in team_conversations:
438 | await ShareFilesManager.copy_file_to_conversation(
439 | context=context,
440 | share_id=share_id,
441 | filename=file.filename,
442 | target_conversation_id=team_conv_id,
443 | )
444 |
445 | # 3. Update all UIs but don't send notifications to reduce noise
446 | await Notifications.notify_all_state_update(context, share_id, [InspectorTab.DEBUG])
447 |
448 | await ShareStorage.log_share_event(
449 | context=context,
450 | share_id=share_id,
451 | entry_type="file_shared",
452 | message=f"File updated: {file.filename}",
453 | metadata={
454 | "file_id": getattr(file, "id", ""),
455 | "filename": file.filename,
456 | "is_coordinator_file": role.value == "coordinator",
457 | },
458 | )
459 |
460 | except Exception as e:
461 | logger.exception(f"Error handling file update: {e}")
462 |
463 |
464 | @assistant.events.conversation.file.on_deleted
465 | async def on_file_deleted(
466 | context: ConversationContext,
467 | event: workbench_model.ConversationEvent,
468 | file: workbench_model.File,
469 | ) -> None:
470 | try:
471 | # Get share ID
472 | share_id = await KnowledgeTransferManager.get_share_id(context)
473 | if not share_id or not file.filename:
474 | return
475 |
476 | role = await detect_assistant_role(context)
477 | if role == ConversationRole.COORDINATOR:
478 | # For Coordinator files:
479 | # 1. Delete from share storage
480 | success = await ShareFilesManager.delete_file_from_knowledge_share_storage(
481 | context=context, share_id=share_id, filename=file.filename
482 | )
483 |
484 | if not success:
485 | logger.error(f"Failed to delete file from share storage: {file.filename}")
486 |
487 | # 2. Update all UIs about the deletion but don't send notifications to reduce noise
488 | await Notifications.notify_all_state_update(context, share_id, [InspectorTab.DEBUG])
489 | # Team files don't need special handling
490 |
491 | await ShareStorage.log_share_event(
492 | context=context,
493 | share_id=share_id,
494 | entry_type="file_deleted",
495 | message=f"File deleted: {file.filename}",
496 | metadata={
497 | "file_id": getattr(file, "id", ""),
498 | "filename": file.filename,
499 | "is_coordinator_file": role.value == "coordinator",
500 | },
501 | )
502 |
503 | except Exception as e:
504 | logger.exception(f"Error handling file deletion: {e}")
505 |
506 |
507 | @assistant.events.conversation.participant.on_created
508 | async def on_participant_joined(
509 | context: ConversationContext,
510 | event: ConversationEvent,
511 | participant: workbench_model.ConversationParticipant,
512 | ) -> None:
513 | try:
514 | if participant.id == context.assistant.id:
515 | return
516 |
517 | # Open the Brief tab (state inspector).
518 | await context.send_conversation_state_event(
519 | AssistantStateEvent(
520 | state_id="brief",
521 | event="focus",
522 | state=None,
523 | )
524 | )
525 |
526 | role = await detect_assistant_role(context)
527 | if role != ConversationRole.TEAM:
528 | return
529 |
530 | share_id = await ConversationKnowledgePackageManager.get_associated_share_id(context)
531 | if not share_id:
532 | return
533 |
534 | await ShareFilesManager.synchronize_files_to_team_conversation(context=context, share_id=share_id)
535 |
536 | await ShareStorage.log_share_event(
537 | context=context,
538 | share_id=share_id,
539 | entry_type=LogEntryType.PARTICIPANT_JOINED,
540 | message=f"Participant joined: {participant.name}",
541 | metadata={
542 | "participant_id": participant.id,
543 | "participant_name": participant.name,
544 | "conversation_id": str(context.id),
545 | },
546 | )
547 |
548 | except Exception as e:
549 | logger.exception(f"Error handling participant join event: {e}")
550 |
```
--------------------------------------------------------------------------------
/assistants/explorer-assistant/assistant/response/response.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | # Prospector Assistant
4 | #
5 | # This assistant helps you mine ideas from artifacts.
6 | #
7 |
8 | import logging
9 | import re
10 | import time
11 | from typing import Any, Awaitable, Callable, Sequence
12 |
13 | import deepmerge
14 | from assistant_extensions.artifacts import ArtifactsExtension
15 | from assistant_extensions.attachments import AttachmentsExtension
16 | from llm_client.model import CompletionMessage
17 | from semantic_workbench_api_model.workbench_model import (
18 | ConversationMessage,
19 | ConversationParticipant,
20 | MessageType,
21 | NewConversationMessage,
22 | )
23 | from semantic_workbench_assistant.assistant_app import (
24 | ConversationContext,
25 | )
26 |
27 | from ..config import AssistantConfigModel
28 | from .model import NumberTokensResult, ResponseProvider
29 | from .response_anthropic import AnthropicResponseProvider
30 | from .response_openai import OpenAIResponseProvider
31 |
32 | logger = logging.getLogger(__name__)
33 |
34 |
35 | #
36 | # region Response
37 | #
38 |
39 |
40 | # demonstrates how to respond to a conversation message using the OpenAI API.
41 | async def respond_to_conversation(
42 | artifacts_extension: ArtifactsExtension,
43 | attachments_extension: AttachmentsExtension,
44 | context: ConversationContext,
45 | config: AssistantConfigModel,
46 | metadata: dict[str, Any] = {},
47 | ) -> None:
48 | """
49 | Respond to a conversation message.
50 |
51 | This method uses the OpenAI API to generate a response to the message.
52 |
53 | It includes any attachments as individual system messages before the chat history, along with references
54 | to the attachments in the point in the conversation where they were mentioned. This allows the model to
55 | consider the full contents of the attachments separate from the conversation, but with the context of
56 | where they were mentioned and any relevant surrounding context such as how to interpret the attachment
57 | or why it was shared or what to do with it.
58 | """
59 |
60 | response_provider = (
61 | AnthropicResponseProvider(assistant_config=config, anthropic_client_config=config.ai_client_config)
62 | if config.ai_client_config.ai_service_type == "anthropic"
63 | else OpenAIResponseProvider(
64 | artifacts_extension=artifacts_extension,
65 | conversation_context=context,
66 | assistant_config=config,
67 | openai_client_config=config.ai_client_config,
68 | )
69 | )
70 |
71 | request_config = config.ai_client_config.request_config
72 |
73 | # define the metadata key for any metadata created within this method
74 | method_metadata_key = "respond_to_conversation"
75 |
76 | # track the start time of the response generation
77 | response_start_time = time.time()
78 |
79 | # get the list of conversation participants
80 | participants_response = await context.get_participants(include_inactive=True)
81 |
82 | # establish a token to be used by the AI model to indicate no response
83 | silence_token = "{{SILENCE}}"
84 |
85 | system_message_content = f'{config.instruction_prompt}\n\nYour name is "{context.assistant.name}".'
86 | if len(participants_response.participants) > 2:
87 | system_message_content += (
88 | "\n\n"
89 | f"There are {len(participants_response.participants)} participants in the conversation,"
90 | " including you as the assistant and the following users:"
91 | + ",".join([
92 | f' "{participant.name}"'
93 | for participant in participants_response.participants
94 | if participant.id != context.assistant.id
95 | ])
96 | + "\n\nYou do not need to respond to every message. Do not respond if the last thing said was a closing"
97 | " statement such as 'bye' or 'goodbye', or just a general acknowledgement like 'ok' or 'thanks'. Do not"
98 | f' respond as another user in the conversation, only as "{context.assistant.name}".'
99 | " Sometimes the other users need to talk amongst themselves and that is ok. If the conversation seems to"
100 | f' be directed at you or the general audience, go ahead and respond.\n\nSay "{silence_token}" to skip'
101 | " your turn."
102 | )
103 |
104 | # add the artifact agent instruction prompt to the system message content
105 | if config.extensions_config.artifacts.enabled:
106 | system_message_content += f"\n\n{config.extensions_config.artifacts.instruction_prompt}"
107 |
108 | # add the guardrails prompt to the system message content
109 | system_message_content += f"\n\n{config.guardrails_prompt}"
110 |
111 | # initialize the completion messages with the system message
112 | completion_messages: list[CompletionMessage] = [
113 | CompletionMessage(
114 | role="system",
115 | content=system_message_content,
116 | )
117 | ]
118 |
119 | token_count = 0
120 |
121 | # calculate the token count for the messages so far
122 | result = await _num_tokens_from_messages(
123 | context=context,
124 | response_provider=response_provider,
125 | messages=completion_messages,
126 | model=request_config.model,
127 | metadata=metadata,
128 | metadata_key="system_message",
129 | )
130 | if result is not None:
131 | token_count += result.count
132 | else:
133 | return
134 |
135 | # generate the attachment messages from the attachment agent
136 | attachment_messages = await attachments_extension.get_completion_messages_for_attachments(
137 | context,
138 | config=config.extensions_config.attachments,
139 | )
140 | result = await _num_tokens_from_messages(
141 | context=context,
142 | response_provider=response_provider,
143 | messages=attachment_messages,
144 | model=request_config.model,
145 | metadata=metadata,
146 | metadata_key="attachment_messages",
147 | )
148 | if result is not None:
149 | token_count += result.count
150 | else:
151 | return
152 |
153 | # calculate the total available tokens for the response generation
154 | available_tokens = request_config.max_tokens - request_config.response_tokens
155 |
156 | history_messages = await _get_history_messages(
157 | response_provider=response_provider,
158 | context=context,
159 | participants=participants_response.participants,
160 | converter=_conversation_message_to_completion_messages,
161 | model=request_config.model,
162 | token_limit=available_tokens - token_count,
163 | )
164 |
165 | # add the attachment messages to the completion messages, either inline or as separate messages
166 | if config.use_inline_attachments:
167 | # inject the attachment messages inline into the history messages
168 | history_messages = _inject_attachments_inline(history_messages, attachment_messages)
169 | else:
170 | # add the attachment messages to the completion messages before the history messages
171 | completion_messages.extend(attachment_messages)
172 |
173 | # add the history messages to the completion messages
174 | completion_messages.extend(history_messages)
175 |
176 | result = await _num_tokens_from_messages(
177 | context=context,
178 | response_provider=response_provider,
179 | messages=completion_messages,
180 | model=request_config.model,
181 | metadata=metadata,
182 | metadata_key=method_metadata_key,
183 | )
184 | if result is not None:
185 | estimated_token_count = result.count
186 | if estimated_token_count > request_config.max_tokens:
187 | await context.send_messages(
188 | NewConversationMessage(
189 | content=(
190 | f"You've exceeded the token limit of {request_config.max_tokens} in this conversation ({estimated_token_count})."
191 | " This assistant does not support recovery from this state."
192 | " Please start a new conversation and let us know you ran into this."
193 | ),
194 | message_type=MessageType.chat,
195 | )
196 | )
197 | return
198 | else:
199 | return
200 |
201 | # set default response message type
202 | message_type = MessageType.chat
203 |
204 | # generate a response from the AI model
205 | response_result = await response_provider.get_response(
206 | messages=completion_messages,
207 | metadata_key=method_metadata_key,
208 | )
209 | content = response_result.content
210 | message_type = response_result.message_type
211 | completion_total_tokens = response_result.completion_total_tokens
212 |
213 | deepmerge.always_merger.merge(metadata, response_result.metadata)
214 |
215 | # create the footer items for the response
216 | footer_items = []
217 |
218 | # add the token usage message to the footer items
219 | if completion_total_tokens > 0:
220 | footer_items.append(_get_token_usage_message(request_config.max_tokens, completion_total_tokens))
221 |
222 | # track the end time of the response generation
223 | response_end_time = time.time()
224 | response_duration = response_end_time - response_start_time
225 |
226 | # add the response duration to the footer items
227 | footer_items.append(_get_response_duration_message(response_duration))
228 |
229 | # update the metadata with the footer items
230 | deepmerge.always_merger.merge(
231 | metadata,
232 | {
233 | "footer_items": footer_items,
234 | },
235 | )
236 |
237 | if content:
238 | # strip out the username from the response
239 | if content.startswith("["):
240 | content = re.sub(r"\[.*\]:\s", "", content)
241 |
242 | # model sometimes puts extra spaces in the response, so remove them
243 | # when checking for the silence token
244 | if content.replace(" ", "") == silence_token:
245 | # if debug output is enabled, notify the conversation that the assistant chose to remain silent
246 | if config.enable_debug_output:
247 | # add debug metadata to indicate the assistant chose to remain silent
248 | deepmerge.always_merger.merge(
249 | metadata,
250 | {
251 | "debug": {
252 | method_metadata_key: {
253 | "silence_token": True,
254 | },
255 | },
256 | "attribution": "debug output",
257 | "generated_content": False,
258 | },
259 | )
260 | # send a notice message to the conversation
261 | await context.send_messages(
262 | NewConversationMessage(
263 | message_type=MessageType.notice,
264 | content="[assistant chose to remain silent]",
265 | metadata=metadata,
266 | )
267 | )
268 | return
269 |
270 | # override message type if content starts with /
271 | if content.startswith("/"):
272 | message_type = MessageType.command_response
273 |
274 | # send the response to the conversation
275 | await context.send_messages(
276 | NewConversationMessage(
277 | content=content or "[no response from openai]",
278 | message_type=message_type,
279 | metadata=metadata,
280 | )
281 | )
282 |
283 | # check the token usage and send a warning if it is high
284 | if completion_total_tokens and config.high_token_usage_warning.enabled:
285 | # calculate the token count for the warning threshold
286 | token_count_for_warning = request_config.max_tokens * (config.high_token_usage_warning.threshold / 100)
287 |
288 | # check if the completion total tokens exceed the warning threshold
289 | if completion_total_tokens > token_count_for_warning:
290 | content = f"{config.high_token_usage_warning.message}\n\nTotal tokens used: {completion_total_tokens}"
291 |
292 | # send a notice message to the conversation that the token usage is high
293 | await context.send_messages(
294 | NewConversationMessage(
295 | content=content,
296 | message_type=MessageType.notice,
297 | metadata={
298 | "debug": {
299 | "high_token_usage_warning": {
300 | "completion_total_tokens": completion_total_tokens,
301 | "threshold": config.high_token_usage_warning.threshold,
302 | "token_count_for_warning": token_count_for_warning,
303 | }
304 | },
305 | "attribution": "system",
306 | },
307 | )
308 | )
309 |
310 |
311 | # endregion
312 |
313 |
314 | #
315 | # region Helpers
316 | #
317 |
318 | # TODO: move to a common module, such as either the openai_client or attachment module for easy re-use in other assistants
319 |
320 |
321 | async def _num_tokens_from_messages(
322 | context: ConversationContext,
323 | response_provider: ResponseProvider,
324 | messages: Sequence[CompletionMessage],
325 | model: str,
326 | metadata: dict[str, Any],
327 | metadata_key: str,
328 | ) -> NumberTokensResult | None:
329 | """
330 | Calculate the number of tokens required to generate the completion messages.
331 | """
332 | try:
333 | return await response_provider.num_tokens_from_messages(
334 | messages=messages, model=model, metadata_key=metadata_key
335 | )
336 | except Exception as e:
337 | logger.exception(f"exception occurred calculating token count: {e}")
338 | deepmerge.always_merger.merge(
339 | metadata,
340 | {
341 | "debug": {
342 | metadata_key: {
343 | "num_tokens_from_messages": {
344 | "request": {
345 | "messages": messages,
346 | "model": model,
347 | },
348 | "error": str(e),
349 | },
350 | },
351 | }
352 | },
353 | )
354 | await context.send_messages(
355 | NewConversationMessage(
356 | content="An error occurred while calculating the token count for the messages.",
357 | message_type=MessageType.notice,
358 | metadata=metadata,
359 | )
360 | )
361 |
362 |
363 | async def _conversation_message_to_completion_messages(
364 | context: ConversationContext, message: ConversationMessage, participants: list[ConversationParticipant]
365 | ) -> list[CompletionMessage]:
366 | """
367 | Convert a conversation message to a list of completion messages.
368 | """
369 |
370 | # some messages may have multiple parts, such as a text message with an attachment
371 | completion_messages: list[CompletionMessage] = []
372 |
373 | # add the message to the completion messages, treating any message from a source other than the assistant
374 | # as a user message
375 | if message.sender.participant_id == context.assistant.id:
376 | completion_messages.append(CompletionMessage(role="assistant", content=_format_message(message, participants)))
377 |
378 | else:
379 | # add the user message to the completion messages
380 | completion_messages.append(CompletionMessage(role="user", content=_format_message(message, participants)))
381 |
382 | if message.filenames and len(message.filenames) > 0:
383 | # add a system message to indicate the attachments
384 | completion_messages.append(
385 | CompletionMessage(role="system", content=f"Attachment(s): {', '.join(message.filenames)}")
386 | )
387 |
388 | return completion_messages
389 |
390 |
391 | async def _get_history_messages(
392 | response_provider: ResponseProvider,
393 | context: ConversationContext,
394 | participants: list[ConversationParticipant],
395 | converter: Callable[
396 | [ConversationContext, ConversationMessage, list[ConversationParticipant]],
397 | Awaitable[list[CompletionMessage]],
398 | ],
399 | model: str,
400 | token_limit: int | None = None,
401 | ) -> list[CompletionMessage]:
402 | """
403 | Get all messages in the conversation, formatted for use in a completion.
404 | """
405 |
406 | # each call to get_messages will return a maximum of 100 messages
407 | # so we need to loop until all messages are retrieved
408 | # if token_limit is provided, we will stop when the token limit is reached
409 |
410 | history = []
411 | token_count = 0
412 | before_message_id = None
413 |
414 | while True:
415 | # get the next batch of messages
416 | messages_response = await context.get_messages(limit=100, before=before_message_id)
417 | messages_list = messages_response.messages
418 |
419 | # if there are no more messages, break the loop
420 | if not messages_list or messages_list.count == 0:
421 | break
422 |
423 | # set the before_message_id for the next batch of messages
424 | before_message_id = messages_list[0].id
425 |
426 | # messages are returned in reverse order, so we need to reverse them
427 | for message in reversed(messages_list):
428 | # format the message
429 | formatted_message_list = await converter(context, message, participants)
430 | try:
431 | results = await _num_tokens_from_messages(
432 | context=context,
433 | response_provider=response_provider,
434 | messages=formatted_message_list,
435 | model=model,
436 | metadata={},
437 | metadata_key="get_history_messages",
438 | )
439 | if results is not None:
440 | token_count += results.count
441 | except Exception as e:
442 | logger.exception(f"exception occurred calculating token count: {e}")
443 |
444 | # if a token limit is provided and the token count exceeds the limit, break the loop
445 | if token_limit and token_count > token_limit:
446 | break
447 |
448 | # insert the formatted messages into the beginning of the history list
449 | history = formatted_message_list + history
450 |
451 | # return the formatted messages
452 | return history
453 |
454 |
455 | def _inject_attachments_inline(
456 | history_messages: list[CompletionMessage],
457 | attachment_messages: Sequence[CompletionMessage],
458 | ) -> list[CompletionMessage]:
459 | """
460 | Inject the attachment messages inline into the history messages.
461 | """
462 |
463 | # iterate over the history messages and for every message that contains an attachment,
464 | # find the related attachment message and replace the attachment message with the inline attachment content
465 | for index, history_message in enumerate(history_messages):
466 | # if the history message does not contain content, as a string value, skip
467 | content = history_message.content
468 | if not content or not isinstance(content, str):
469 | # TODO: handle list content, which may contain multiple parts including images
470 | continue
471 |
472 | # get the attachment filenames string from the history message content
473 | attachment_filenames_string = re.findall(r"Attachment\(s\): (.+)", content)
474 |
475 | # if the history message does not contain an attachment filenames string, skip
476 | if not attachment_filenames_string:
477 | continue
478 |
479 | # split the attachment filenames string into a list of attachment filenames
480 | attachment_filenames = [filename.strip() for filename in attachment_filenames_string[0].split(",")]
481 |
482 | # initialize a list to store the replacement messages
483 | replacement_messages = []
484 |
485 | # iterate over the attachment filenames and find the related attachment message
486 | for attachment_filename in attachment_filenames:
487 | # find the related attachment message
488 | attachment_message = next(
489 | (
490 | attachment_message
491 | for attachment_message in attachment_messages
492 | if f"<ATTACHMENT><FILENAME>{attachment_filename}</FILENAME>" in str(attachment_message.content)
493 | ),
494 | None,
495 | )
496 |
497 | if attachment_message:
498 | # replace the attachment message with the inline attachment content
499 | replacement_messages.append(attachment_message)
500 |
501 | # if there are replacement messages, replace the history message with the replacement messages
502 | if len(replacement_messages) > 0:
503 | history_messages[index : index + 1] = replacement_messages
504 |
505 | return history_messages
506 |
507 |
508 | def _get_response_duration_message(response_duration: float) -> str:
509 | """
510 | Generate a display friendly message for the response duration, to be added to the footer items.
511 | """
512 |
513 | return f"Response time: {response_duration:.2f} seconds"
514 |
515 |
516 | def _get_token_usage_message(
517 | max_tokens: int,
518 | completion_total_tokens: int,
519 | ) -> str:
520 | """
521 | Generate a display friendly message for the token usage, to be added to the footer items.
522 | """
523 |
524 | def get_display_count(tokens: int) -> str:
525 | # if less than 1k, return the number of tokens
526 | # if greater than or equal to 1k, return the number of tokens in k
527 | # use 1 decimal place for k
528 | # drop the decimal place if the number of tokens in k is a whole number
529 | if tokens < 1000:
530 | return str(tokens)
531 | else:
532 | tokens_in_k = tokens / 1000
533 | if tokens_in_k.is_integer():
534 | return f"{int(tokens_in_k)}k"
535 | else:
536 | return f"{tokens_in_k:.1f}k"
537 |
538 | return f"Tokens used: {get_display_count(completion_total_tokens)} of {get_display_count(max_tokens)} ({int(completion_total_tokens / max_tokens * 100)}%)"
539 |
540 |
541 | def _format_message(message: ConversationMessage, participants: list[ConversationParticipant]) -> str:
542 | """
543 | Format a conversation message for display.
544 | """
545 | conversation_participant = next(
546 | (participant for participant in participants if participant.id == message.sender.participant_id),
547 | None,
548 | )
549 | participant_name = conversation_participant.name if conversation_participant else "unknown"
550 | message_datetime = message.timestamp.strftime("%Y-%m-%d %H:%M:%S")
551 | return f"[{participant_name} - {message_datetime}]: {message.content}"
552 |
553 |
554 | # endregion
555 |
```
--------------------------------------------------------------------------------
/workbench-service/tests/test_integration.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import io
3 | import json
4 | import logging
5 | import re
6 | import uuid
7 |
8 | import httpx
9 | import pytest
10 | import semantic_workbench_assistant.canonical
11 | from asgi_lifespan import LifespanManager
12 | from fastapi import FastAPI
13 | from semantic_workbench_api_model import assistant_model, workbench_model
14 |
15 | from .types import MockUser
16 |
17 |
18 | async def wait_for_assistant_service_registration(
19 | wb_client: httpx.AsyncClient,
20 | ) -> workbench_model.AssistantServiceRegistration:
21 | for _ in range(10):
22 | http_response = await wb_client.get("/assistant-service-registrations")
23 | http_response.raise_for_status()
24 | assistant_services = workbench_model.AssistantServiceRegistrationList.model_validate(http_response.json())
25 | if assistant_services.assistant_service_registrations:
26 | return assistant_services.assistant_service_registrations[0]
27 |
28 | await asyncio.sleep(0.01)
29 |
30 | raise Exception("Timed out waiting for assistant service registration")
31 |
32 |
33 | async def test_flow_create_assistant_update_config(
34 | workbench_service: FastAPI,
35 | canonical_assistant_service: FastAPI,
36 | test_user: MockUser,
37 | ) -> None:
38 | async with (
39 | LifespanManager(workbench_service),
40 | httpx.AsyncClient(
41 | transport=httpx.ASGITransport(app=workbench_service),
42 | headers=test_user.authorization_headers,
43 | base_url="http://test",
44 | ) as wb_client,
45 | LifespanManager(canonical_assistant_service),
46 | ):
47 | assistant_service = await wait_for_assistant_service_registration(wb_client)
48 |
49 | resp = await wb_client.post(
50 | "/assistants",
51 | json=workbench_model.NewAssistant(
52 | name="test-assistant",
53 | assistant_service_id=assistant_service.assistant_service_id,
54 | ).model_dump(mode="json"),
55 | )
56 | logging.info("POST wb/assistants resp: %s", resp.json())
57 | resp.raise_for_status()
58 |
59 | assistant = workbench_model.Assistant(**resp.json())
60 | logging.info("POST wb/assistants resp loaded into model: %s", assistant)
61 |
62 | resp = await wb_client.get(f"/assistants/{assistant.id}")
63 | logging.info("GET wb/assistant/id resp: %s", resp.json())
64 | resp.raise_for_status()
65 |
66 | assert resp.json() == json.loads(assistant.model_dump_json())
67 |
68 | config = assistant_model.ConfigPutRequestModel(
69 | config=semantic_workbench_assistant.canonical.ConfigStateModel(
70 | short_text="test short text",
71 | long_text="test long text",
72 | prompt=semantic_workbench_assistant.canonical.PromptConfigModel(
73 | custom_prompt="test custom prompt",
74 | temperature=0.999999,
75 | ),
76 | ).model_dump(),
77 | )
78 | resp = await wb_client.put(f"/assistants/{assistant.id}/config", json=config.model_dump(mode="json"))
79 | resp.raise_for_status()
80 |
81 |
82 | async def test_flow_create_assistant_update_conversation_state(
83 | workbench_service: FastAPI,
84 | canonical_assistant_service: FastAPI,
85 | test_user: MockUser,
86 | ) -> None:
87 | async with (
88 | LifespanManager(workbench_service),
89 | httpx.AsyncClient(
90 | transport=httpx.ASGITransport(app=workbench_service),
91 | headers=test_user.authorization_headers,
92 | base_url="http://test",
93 | ) as wb_client,
94 | LifespanManager(canonical_assistant_service),
95 | ):
96 | assistant_service = await wait_for_assistant_service_registration(wb_client)
97 |
98 | resp = await wb_client.post(
99 | "/assistants",
100 | json=workbench_model.NewAssistant(
101 | name="test-assistant",
102 | assistant_service_id=assistant_service.assistant_service_id,
103 | ).model_dump(mode="json"),
104 | )
105 | resp.raise_for_status()
106 | logging.info("POST wb/assistants resp: %s", resp.json())
107 |
108 | assistant = workbench_model.Assistant.model_validate(resp.json())
109 | logging.info("POST wb/assistants resp loaded into model: %s", assistant)
110 |
111 | resp = await wb_client.get(f"/assistants/{assistant.id}")
112 | resp.raise_for_status()
113 | logging.info("GET wb/assistant/id resp: %s", resp.json())
114 |
115 | assert resp.json() == json.loads(assistant.model_dump_json())
116 |
117 | resp = await wb_client.post("/conversations", json={"title": "test-conversation"})
118 | resp.raise_for_status()
119 | conversation = workbench_model.Conversation.model_validate(resp.json())
120 |
121 | resp = await wb_client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
122 | resp.raise_for_status()
123 | participant = workbench_model.ConversationParticipant.model_validate(resp.json())
124 | assert participant.online is True
125 |
126 | resp = await wb_client.get(f"/assistants/{assistant.id}/conversations/{conversation.id}/states")
127 | resp.raise_for_status()
128 | logging.info("GET asst/conversations/id/states resp: %s", resp.json())
129 |
130 | states = assistant_model.StateDescriptionListResponseModel(**resp.json())
131 | logging.info("GET asst/conversations/id/states resp loaded into model: %s", states)
132 |
133 | assert len(states.states) == 1
134 | assert states.states[0].id == "simple_state"
135 |
136 | resp = await wb_client.get(f"/assistants/{assistant.id}/conversations/{conversation.id}/states/simple_state")
137 | resp.raise_for_status()
138 | logging.info("GET asst/conversations/id/states/simple_state resp: %s", resp.json())
139 |
140 | state = assistant_model.StateResponseModel(**resp.json())
141 | logging.info("GET asst/conversations/id/states/simple_state resp loaded into model: %s", state)
142 |
143 | assert "message" in state.data
144 |
145 | updated_message = f"updated message {uuid.uuid4()}"
146 | state_update = assistant_model.StatePutRequestModel(
147 | data={"message": updated_message},
148 | )
149 | resp = await wb_client.put(
150 | f"/assistants/{assistant.id}/conversations/{conversation.id}/states/simple_state",
151 | json=state_update.model_dump(mode="json"),
152 | )
153 | resp.raise_for_status()
154 |
155 | resp = await wb_client.get(f"/assistants/{assistant.id}/conversations/{conversation.id}/states/simple_state")
156 | resp.raise_for_status()
157 | logging.info("GET asst/conversations/id/states/simple_state resp: %s", resp.json())
158 |
159 | state = assistant_model.StateResponseModel(**resp.json())
160 | logging.info("GET asst/conversations/id/states/simple_state resp loaded into model: %s", state)
161 |
162 | assert "message" in state.data
163 | assert state.data["message"] == updated_message
164 |
165 |
166 | async def test_flow_create_assistant_send_message_receive_resp(
167 | workbench_service: FastAPI,
168 | canonical_assistant_service: FastAPI,
169 | test_user: MockUser,
170 | ) -> None:
171 | async with (
172 | LifespanManager(workbench_service),
173 | httpx.AsyncClient(
174 | transport=httpx.ASGITransport(app=workbench_service),
175 | headers=test_user.authorization_headers,
176 | base_url="http://test",
177 | ) as wb_client,
178 | LifespanManager(canonical_assistant_service),
179 | ):
180 | assistant_service = await wait_for_assistant_service_registration(wb_client)
181 |
182 | resp = await wb_client.post(
183 | "/assistants",
184 | json=workbench_model.NewAssistant(
185 | name="test-assistant",
186 | assistant_service_id=assistant_service.assistant_service_id,
187 | ).model_dump(mode="json"),
188 | )
189 | resp.raise_for_status()
190 | logging.info("POST wb/assistants resp: %s", resp.json())
191 |
192 | assistant = workbench_model.Assistant(**resp.json())
193 |
194 | resp = await wb_client.post("/conversations", json={"title": "test-conversation"})
195 | resp.raise_for_status()
196 | conversation = workbench_model.Conversation.model_validate(resp.json())
197 |
198 | resp = await wb_client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
199 | resp.raise_for_status()
200 |
201 | resp = await wb_client.post(
202 | f"/conversations/{conversation.id}/messages",
203 | json={"content": "hello"},
204 | )
205 | resp.raise_for_status()
206 | logging.info("POST wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
207 |
208 | attempts = 1
209 | messages = []
210 | while attempts <= 10 and len(messages) < 2:
211 | if attempts > 1:
212 | await asyncio.sleep(0.5)
213 | attempts += 1
214 |
215 | resp = await wb_client.get(f"/conversations/{conversation.id}/messages")
216 | resp.raise_for_status()
217 | logging.info("GET wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
218 |
219 | messages_resp = resp.json()
220 |
221 | assert "messages" in messages_resp
222 | messages = messages_resp["messages"]
223 |
224 | assert len(messages) > 1
225 |
226 |
227 | async def test_flow_create_assistant_send_message_receive_resp_export_import_assistant(
228 | workbench_service: FastAPI,
229 | canonical_assistant_service: FastAPI,
230 | test_user: MockUser,
231 | ) -> None:
232 | async with (
233 | LifespanManager(workbench_service),
234 | httpx.AsyncClient(
235 | transport=httpx.ASGITransport(app=workbench_service),
236 | headers=test_user.authorization_headers,
237 | base_url="http://test",
238 | ) as wb_client,
239 | LifespanManager(canonical_assistant_service),
240 | ):
241 | assistant_service = await wait_for_assistant_service_registration(wb_client)
242 |
243 | resp = await wb_client.post(
244 | "/assistants",
245 | json=workbench_model.NewAssistant(
246 | name="test-assistant",
247 | assistant_service_id=assistant_service.assistant_service_id,
248 | ).model_dump(mode="json"),
249 | )
250 | resp.raise_for_status()
251 | logging.info("POST wb/assistants resp: %s", resp.json())
252 |
253 | assistant = workbench_model.Assistant(**resp.json())
254 |
255 | resp = await wb_client.post("/conversations", json={"title": "test-conversation"})
256 | resp.raise_for_status()
257 | conversation = workbench_model.Conversation.model_validate(resp.json())
258 |
259 | resp = await wb_client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
260 | resp.raise_for_status()
261 |
262 | async def send_message_wait_for_response(conversation: workbench_model.Conversation) -> None:
263 | resp = await wb_client.get(f"/conversations/{conversation.id}/messages")
264 | resp.raise_for_status()
265 | existing_messages = workbench_model.ConversationMessageList.model_validate(resp.json())
266 |
267 | logging.info("POST wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
268 | resp = await wb_client.post(
269 | f"/conversations/{conversation.id}/messages",
270 | json={"content": "hello"},
271 | )
272 | resp.raise_for_status()
273 | logging.info("POST wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
274 |
275 | url = f"/conversations/{conversation.id}/messages"
276 | params = {}
277 | if existing_messages.messages:
278 | params = {"after": str(existing_messages.messages[-1].id)}
279 | attempts = 1
280 | messages = []
281 | while attempts <= 10 and len(messages) < 2:
282 | if attempts > 1:
283 | await asyncio.sleep(0.5)
284 |
285 | attempts += 1
286 |
287 | resp = await wb_client.get(url, params=params)
288 | resp.raise_for_status()
289 | logging.info("GET %s resp: %s", url, resp.json())
290 |
291 | messages_response = workbench_model.ConversationMessageList.model_validate(resp.json())
292 | messages = messages_response.messages
293 |
294 | assert len(messages) == 2
295 | assert messages[0].sender.participant_role == workbench_model.ParticipantRole.user
296 | assert messages[1].sender.participant_role == workbench_model.ParticipantRole.assistant
297 |
298 | await send_message_wait_for_response(conversation)
299 |
300 | resp = await wb_client.get(f"/assistants/{assistant.id}/export")
301 | resp.raise_for_status()
302 |
303 | assert resp.headers["content-type"] == "application/zip"
304 | assert "content-length" in resp.headers
305 | assert int(resp.headers["content-length"]) > 0
306 |
307 | logging.info("response: %s", resp.content)
308 |
309 | exported_file = io.BytesIO(resp.content)
310 |
311 | for import_number in range(1, 3):
312 | resp = await wb_client.post("/conversations/import", files={"from_export": exported_file})
313 | logging.info("import %s response: %s", import_number, resp.json())
314 | resp.raise_for_status()
315 |
316 | import_result = workbench_model.ConversationImportResult.model_validate(resp.json())
317 | new_assistant_id = import_result.assistant_ids[0]
318 |
319 | resp = await wb_client.get(f"/assistants/{new_assistant_id}/conversations")
320 | conversations = workbench_model.ConversationList.model_validate(resp.json())
321 | new_conversation = conversations.conversations[0]
322 |
323 | resp = await wb_client.get("/assistants")
324 | logging.info("response: %s", resp.json())
325 | resp.raise_for_status()
326 | assistants_response = workbench_model.AssistantList.model_validate(resp.json())
327 | assistant_count = len(assistants_response.assistants)
328 | assert assistant_count == 1
329 | assert assistants_response.assistants[0].name == "test-assistant"
330 |
331 | # ensure the new assistant can send and receive messages in the new conversation
332 | await send_message_wait_for_response(new_conversation)
333 |
334 |
335 | async def test_flow_create_assistant_send_message_receive_resp_export_import_conversations(
336 | workbench_service: FastAPI,
337 | canonical_assistant_service: FastAPI,
338 | test_user: MockUser,
339 | ) -> None:
340 | async with (
341 | LifespanManager(workbench_service),
342 | httpx.AsyncClient(
343 | transport=httpx.ASGITransport(app=workbench_service),
344 | headers=test_user.authorization_headers,
345 | base_url="http://test",
346 | ) as wb_client,
347 | LifespanManager(canonical_assistant_service),
348 | ):
349 | assistant_service = await wait_for_assistant_service_registration(wb_client)
350 |
351 | resp = await wb_client.post(
352 | "/assistants",
353 | json=workbench_model.NewAssistant(
354 | name="test-assistant",
355 | assistant_service_id=assistant_service.assistant_service_id,
356 | ).model_dump(mode="json"),
357 | )
358 | resp.raise_for_status()
359 | logging.info("POST wb/assistants resp: %s", resp.json())
360 |
361 | assistant = workbench_model.Assistant(**resp.json())
362 |
363 | resp = await wb_client.post("/conversations", json={"title": "test-conversation"})
364 | resp.raise_for_status()
365 | conversation = workbench_model.Conversation.model_validate(resp.json())
366 |
367 | resp = await wb_client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
368 | resp.raise_for_status()
369 |
370 | async def send_message_wait_for_response(conversation: workbench_model.Conversation) -> None:
371 | resp = await wb_client.get(f"/conversations/{conversation.id}/messages")
372 | resp.raise_for_status()
373 | existing_messages = workbench_model.ConversationMessageList.model_validate(resp.json())
374 |
375 | resp = await wb_client.post(
376 | f"/conversations/{conversation.id}/messages",
377 | json={"content": "hello"},
378 | )
379 | resp.raise_for_status()
380 | logging.info("POST wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
381 |
382 | url = f"/conversations/{conversation.id}/messages"
383 | params = {}
384 | if existing_messages.messages:
385 | params = {"after": str(existing_messages.messages[-1].id)}
386 | attempts = 1
387 | messages = []
388 | while attempts <= 10 and len(messages) < 2:
389 | if attempts > 1:
390 | await asyncio.sleep(0.5)
391 |
392 | attempts += 1
393 |
394 | resp = await wb_client.get(url, params=params)
395 | resp.raise_for_status()
396 | logging.info("GET wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
397 |
398 | messages_response = workbench_model.ConversationMessageList.model_validate(resp.json())
399 | messages = messages_response.messages
400 |
401 | assert len(messages) == 2
402 | assert messages[0].sender.participant_role == workbench_model.ParticipantRole.user
403 | assert messages[1].sender.participant_role == workbench_model.ParticipantRole.assistant
404 |
405 | await send_message_wait_for_response(conversation)
406 |
407 | resp = await wb_client.get("/conversations/export", params={"id": str(conversation.id)})
408 | resp.raise_for_status()
409 |
410 | assert resp.headers["content-type"] == "application/zip"
411 | assert "content-length" in resp.headers
412 | assert int(resp.headers["content-length"]) > 0
413 |
414 | logging.info("response: %s", resp.content)
415 |
416 | file_io = io.BytesIO(resp.content)
417 |
418 | for import_number in range(1, 3):
419 | resp = await wb_client.post("/conversations/import", files={"from_export": file_io})
420 | logging.info("import %s response: %s", import_number, resp.json())
421 | resp.raise_for_status()
422 | import_result = workbench_model.ConversationImportResult.model_validate(resp.json())
423 | assert len(import_result.assistant_ids) == 1
424 | new_assistant_id = import_result.assistant_ids[0]
425 |
426 | resp = await wb_client.get(f"/assistants/{new_assistant_id}/conversations")
427 | conversations = workbench_model.ConversationList.model_validate(resp.json())
428 | new_conversation = conversations.conversations[0]
429 |
430 | resp = await wb_client.get("/assistants")
431 | logging.info("response: %s", resp.json())
432 | resp.raise_for_status()
433 |
434 | assistants_response = workbench_model.AssistantList.model_validate(resp.json())
435 | assistant_count = len(assistants_response.assistants)
436 | assert assistant_count == 1
437 |
438 | assert assistants_response.assistants[0].name == "test-assistant"
439 |
440 | # ensure the new assistant can send and receive messages in the new conversation
441 | await send_message_wait_for_response(new_conversation)
442 |
443 |
444 | @pytest.mark.parametrize(
445 | # spell-checker:ignore dlrow olleh
446 | ("command", "command_args", "expected_response_content_regex"),
447 | [
448 | ("/reverse", "hello world", re.compile("dlrow olleh")),
449 | ("/reverse", "-h", re.compile("usage: /reverse.+", re.DOTALL)),
450 | ("/reverse", "", re.compile("/reverse: error: .+", re.DOTALL)),
451 | ],
452 | )
453 | async def test_flow_create_assistant_send_command_message_receive_resp(
454 | workbench_service: FastAPI,
455 | canonical_assistant_service: FastAPI,
456 | test_user: MockUser,
457 | command: str,
458 | command_args: str,
459 | expected_response_content_regex: re.Pattern,
460 | ) -> None:
461 | async with (
462 | LifespanManager(workbench_service),
463 | httpx.AsyncClient(
464 | transport=httpx.ASGITransport(app=workbench_service),
465 | headers=test_user.authorization_headers,
466 | base_url="http://test",
467 | ) as wb_client,
468 | LifespanManager(canonical_assistant_service),
469 | ):
470 | assistant_service = await wait_for_assistant_service_registration(wb_client)
471 |
472 | resp = await wb_client.post(
473 | "/assistants",
474 | json=workbench_model.NewAssistant(
475 | name="test-assistant",
476 | assistant_service_id=assistant_service.assistant_service_id,
477 | ).model_dump(mode="json"),
478 | )
479 | resp.raise_for_status()
480 | logging.info("POST wb/assistants resp: %s", resp.json())
481 | assistant = workbench_model.Assistant.model_validate(resp.json())
482 | logging.info("assistant: %s", assistant)
483 |
484 | resp = await wb_client.post(
485 | "/conversations",
486 | json={"title": "test-assistant"},
487 | )
488 | resp.raise_for_status()
489 | logging.info("POST wb/conversations resp: %s", resp.json())
490 | conversation = workbench_model.Conversation.model_validate(resp.json())
491 |
492 | resp = await wb_client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
493 | resp.raise_for_status()
494 |
495 | command_content = f"{command} {command_args}"
496 | resp = await wb_client.post(
497 | f"/conversations/{conversation.id}/messages",
498 | json={
499 | "message_type": "command",
500 | "content_type": "application/json",
501 | "content": command_content,
502 | },
503 | )
504 | resp.raise_for_status()
505 | logging.info("POST wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
506 |
507 | attempts = 1
508 | messages = []
509 | while attempts <= 10 and len(messages) < 2:
510 | if attempts > 1:
511 | await asyncio.sleep(0.5)
512 | attempts += 1
513 |
514 | resp = await wb_client.get(f"/conversations/{conversation.id}/messages")
515 | resp.raise_for_status()
516 | logging.info("GET wb/conversations/%s/messages resp: %s", conversation.id, resp.json())
517 |
518 | messages_resp = resp.json()
519 |
520 | assert "messages" in messages_resp
521 | messages = messages_resp["messages"]
522 |
523 | assert len(messages) > 1
524 | response_message = messages[1]
525 |
526 | assert expected_response_content_regex.fullmatch(response_message["content"])
527 | assert response_message["message_type"] == "command-response"
528 |
```