This is page 94 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/document-assistant/assistant/filesystem/_inspector.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | import asyncio
4 | import datetime
5 | import io
6 | import logging
7 | import uuid
8 | from contextlib import asynccontextmanager
9 | from hashlib import md5
10 | from typing import Annotated, Any, AsyncGenerator, Callable, Literal, Protocol
11 |
12 | import deepmerge
13 | from assistant_drive import Drive, IfDriveFileExistsBehavior
14 | from pydantic import BaseModel, Field, ValidationError, create_model
15 | from semantic_workbench_api_model import workbench_model
16 | from semantic_workbench_assistant.assistant_app import (
17 | AssistantAppProtocol,
18 | AssistantConversationInspectorStateDataModel,
19 | BaseModelAssistantConfig,
20 | ConversationContext,
21 | )
22 | from semantic_workbench_assistant.config import UISchema, get_ui_schema
23 |
24 | from assistant.config import AssistantConfigModel
25 | from assistant.filesystem._tasks import task_compute_summary
26 |
27 | from ._model import DocumentEditorConfigProvider
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 | assistant_config = BaseModelAssistantConfig(AssistantConfigModel)
32 |
33 |
34 | class DocumentFileStateModel(BaseModel):
35 | filename: Annotated[str, UISchema(readonly=True)]
36 | content: Annotated[str, UISchema(widget="textarea", rows=800, hide_label=True)]
37 |
38 |
39 | def document_list_model(documents: list[DocumentFileStateModel]) -> type[BaseModel]:
40 | filenames = [document.filename for document in documents]
41 |
42 | return create_model(
43 | "DocumentListModel",
44 | active_document=Annotated[
45 | Literal[tuple(filenames)],
46 | Field(
47 | title="Edit a document:",
48 | description="Select a document and click Edit .",
49 | ),
50 | ],
51 | )
52 |
53 |
54 | def _get_document_editor_ui_schema(readonly: bool, documents: list[DocumentFileStateModel]) -> dict[str, Any]:
55 | schema = get_ui_schema(DocumentFileStateModel)
56 | multiple_files_message = "To edit another document, switch to the Documents tab." if len(documents) > 1 else ""
57 | schema = deepmerge.always_merger.merge(
58 | schema.copy(),
59 | {
60 | "ui:options": {
61 | "collapsible": False,
62 | "title": "Document Editor",
63 | "description": multiple_files_message,
64 | "readonly": readonly,
65 | },
66 | },
67 | )
68 | return schema
69 |
70 |
71 | def _get_document_list_ui_schema(model: type[BaseModel], filenames: list[str]) -> dict[str, Any]:
72 | return {
73 | "ui:options": {
74 | "collapsible": False,
75 | "hideTitle": True,
76 | },
77 | "ui:submitButtonOptions": {
78 | "submitText": "Edit",
79 | "norender": len(filenames) <= 1,
80 | },
81 | "active_document": {
82 | "ui:options": {
83 | "widget": "radio" if len(filenames) > 1 else "hidden",
84 | },
85 | },
86 | }
87 |
88 |
89 | def markdown_content_postprocessor(content: str) -> str:
90 | """
91 | Post-process the markdown content
92 | This is a workaround for the issue where <br /> tags are not rendered correctly in the UI.
93 | """
94 | return content.replace("<br />", " ")
95 |
96 |
97 | class InspectorController(Protocol):
98 | async def is_enabled(self, context: ConversationContext) -> bool: ...
99 | async def is_read_only(self, context: ConversationContext) -> bool: ...
100 | async def read_active_document(self, context: ConversationContext) -> DocumentFileStateModel | None: ...
101 | async def write_active_document(self, context: ConversationContext, content: str) -> None: ...
102 | async def set_active_filename(self, context: ConversationContext, filename: str) -> None: ...
103 | async def list_documents(self, context: ConversationContext) -> list[DocumentFileStateModel]: ...
104 |
105 |
106 | class EditableDocumentFileStateInspector:
107 | def __init__(
108 | self,
109 | controller: InspectorController,
110 | display_name: str,
111 | description: str = "",
112 | ) -> None:
113 | self._state_id = md5(
114 | (type(self).__name__ + "_" + display_name).encode("utf-8"),
115 | usedforsecurity=False,
116 | ).hexdigest()
117 | self._display_name = display_name
118 | self._description = description
119 | self._controller = controller
120 |
121 | @property
122 | def state_id(self) -> str:
123 | return self._state_id
124 |
125 | @property
126 | def display_name(self) -> str:
127 | return self._display_name
128 |
129 | @property
130 | def description(self) -> str:
131 | return self._description
132 |
133 | async def is_enabled(self, context: ConversationContext) -> bool:
134 | return await self._controller.is_enabled(context)
135 |
136 | async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel:
137 | if not await self._controller.is_enabled(context):
138 | return AssistantConversationInspectorStateDataModel(
139 | data={"content": "The Document Editor extension is not enabled."}
140 | )
141 |
142 | document = await self._controller.read_active_document(context)
143 | if not document:
144 | return AssistantConversationInspectorStateDataModel(data={"content": "No current document."})
145 |
146 | is_readonly = await self._controller.is_read_only(context)
147 |
148 | markdown_content = markdown_content_postprocessor(document.content)
149 | return AssistantConversationInspectorStateDataModel(
150 | data={
151 | "markdown_content": markdown_content,
152 | "filename": document.filename,
153 | "readonly": is_readonly,
154 | }
155 | )
156 |
157 | async def set(
158 | self,
159 | context: ConversationContext,
160 | data: dict[str, Any],
161 | ) -> None:
162 | if not await self._controller.is_enabled(context):
163 | return
164 | if await self._controller.is_read_only(context):
165 | return
166 |
167 | # The data comes in with 'markdown_content' but our model expects 'content'
168 | if "markdown_content" in data:
169 | content = data["markdown_content"]
170 | # If filename is present but we don't need to modify it, we can just get the content
171 | await self._controller.write_active_document(context, content)
172 | else:
173 | try:
174 | model = DocumentFileStateModel.model_validate(data)
175 | await self._controller.write_active_document(context, model.content)
176 | except ValidationError:
177 | logger.exception("invalid data for DocumentFileStateModel")
178 | return
179 |
180 |
181 | class ReadonlyDocumentFileStateInspector:
182 | def __init__(
183 | self,
184 | controller: InspectorController,
185 | display_name: str,
186 | description: str = "",
187 | ) -> None:
188 | self._state_id = md5(
189 | (type(self).__name__ + "_" + display_name).encode("utf-8"),
190 | usedforsecurity=False,
191 | ).hexdigest()
192 | self._display_name = display_name
193 | self._description = description
194 | self._controller = controller
195 |
196 | @property
197 | def state_id(self) -> str:
198 | return self._state_id
199 |
200 | @property
201 | def display_name(self) -> str:
202 | return self._display_name
203 |
204 | @property
205 | def description(self) -> str:
206 | return self._description
207 |
208 | async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel:
209 | if not await self._controller.is_enabled(context):
210 | return AssistantConversationInspectorStateDataModel(
211 | data={"content": "The Document Editor extension is not enabled."}
212 | )
213 |
214 | document = await self._controller.read_active_document(context)
215 | if not document:
216 | return AssistantConversationInspectorStateDataModel(data={"content": "No current document."})
217 |
218 | markdown_content = markdown_content_postprocessor(document.content)
219 | return AssistantConversationInspectorStateDataModel(
220 | data={
221 | "markdown_content": markdown_content,
222 | "filename": document.filename,
223 | "readonly": True, # Always read-only for this inspector
224 | },
225 | )
226 |
227 |
228 | class DocumentListInspector:
229 | def __init__(
230 | self,
231 | controller: InspectorController,
232 | display_name: str,
233 | description: str = "",
234 | ) -> None:
235 | self._state_id = md5(
236 | (type(self).__name__ + "_" + display_name).encode("utf-8"),
237 | usedforsecurity=False,
238 | ).hexdigest()
239 | self._display_name = display_name
240 | self._description = description
241 | self._controller = controller
242 |
243 | @property
244 | def state_id(self) -> str:
245 | return self._state_id
246 |
247 | @property
248 | def display_name(self) -> str:
249 | return self._display_name
250 |
251 | @property
252 | def description(self) -> str:
253 | return self._description
254 |
255 | async def is_enabled(self, context: ConversationContext) -> bool:
256 | return await self._controller.is_enabled(context)
257 |
258 | async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel:
259 | if not await self._controller.is_enabled(context):
260 | return AssistantConversationInspectorStateDataModel(
261 | data={"content": "The Document Editor extension is not enabled."}
262 | )
263 |
264 | documents = await self._controller.list_documents(context)
265 | if not documents:
266 | return AssistantConversationInspectorStateDataModel(data={"content": "No documents available."})
267 |
268 | filenames = [document.filename for document in documents]
269 | model = document_list_model(documents)
270 |
271 | current_document = await self._controller.read_active_document(context)
272 | selected_filename = current_document.filename if current_document else filenames[0]
273 |
274 | return AssistantConversationInspectorStateDataModel(
275 | data={
276 | "attachments": [
277 | DocumentFileStateModel.model_validate(document).model_dump(mode="json") for document in documents
278 | ],
279 | "active_document": selected_filename,
280 | },
281 | json_schema=model.model_json_schema(),
282 | ui_schema=_get_document_list_ui_schema(model, filenames),
283 | )
284 |
285 | async def set(
286 | self,
287 | context: ConversationContext,
288 | data: dict[str, Any],
289 | ) -> None:
290 | if not await self._controller.is_enabled(context):
291 | return
292 |
293 | active_document = data.get("active_document")
294 | if not active_document:
295 | return
296 |
297 | await self._controller.set_active_filename(context, active_document)
298 |
299 |
300 | class DocumentInspectors:
301 | def __init__(
302 | self,
303 | app: AssistantAppProtocol,
304 | config_provider: DocumentEditorConfigProvider,
305 | drive_provider: Callable[[ConversationContext], Drive],
306 | ) -> None:
307 | self._config_provider = config_provider
308 | self._drive_provider = drive_provider
309 | self._selected_file: dict[str, str] = {}
310 | self._readonly: set[str] = set()
311 |
312 | self._file_list = DocumentListInspector(
313 | controller=self,
314 | display_name="Documents",
315 | description="Download a document:",
316 | )
317 | app.add_inspector_state_provider(state_id=self._file_list.state_id, provider=self._file_list)
318 |
319 | self._viewer = ReadonlyDocumentFileStateInspector(
320 | controller=self,
321 | display_name="Document Viewer",
322 | )
323 |
324 | self._editor = EditableDocumentFileStateInspector(
325 | controller=self,
326 | display_name="Document Editor",
327 | )
328 | app.add_inspector_state_provider(state_id=self._editor.state_id, provider=self._editor)
329 |
330 | @app.events.conversation.participant.on_updated_including_mine
331 | async def on_participant_update(
332 | ctx: ConversationContext,
333 | event: workbench_model.ConversationEvent,
334 | participant: workbench_model.ConversationParticipant,
335 | ) -> None:
336 | documents_locked = participant.metadata.get("document_lock", None)
337 | if documents_locked is None:
338 | return
339 |
340 | match documents_locked:
341 | case True:
342 | if ctx.id in self._readonly:
343 | return
344 | self._readonly.add(ctx.id)
345 | await self._emit_state_change_event(ctx)
346 |
347 | case False:
348 | if ctx.id not in self._readonly:
349 | return
350 | self._readonly.remove(ctx.id)
351 | await self._emit_state_change_event(ctx)
352 |
353 | async def _emit_state_change_event(self, ctx: ConversationContext) -> None:
354 | for state_id in (
355 | self._editor.state_id,
356 | # self._viewer.state_id,
357 | self._file_list.state_id,
358 | ):
359 | await ctx.send_conversation_state_event(
360 | workbench_model.AssistantStateEvent(
361 | state_id=state_id,
362 | event="updated",
363 | state=None,
364 | )
365 | )
366 |
367 | async def on_external_write(self, context: ConversationContext, filename: str) -> None:
368 | self._selected_file[context.id] = filename
369 |
370 | content = await self.get_file_content(context=context, filename=filename) or ""
371 | config = await assistant_config.get(context.assistant)
372 | asyncio.create_task(
373 | task_compute_summary(context=context, config=config, file_content=content, filename=filename)
374 | )
375 | await context.send_conversation_state_event(
376 | workbench_model.AssistantStateEvent(
377 | state_id=self._editor.state_id,
378 | event="focus",
379 | state=None,
380 | )
381 | )
382 |
383 | async def is_enabled(self, context: ConversationContext) -> bool:
384 | config = await self._config_provider(context)
385 | return config.enabled
386 |
387 | async def is_read_only(self, context: ConversationContext) -> bool:
388 | return context.id in self._readonly
389 |
390 | async def read_active_document(self, context: ConversationContext) -> DocumentFileStateModel | None:
391 | drive = self._drive_provider(context)
392 | markdown_files = [filename for filename in drive.list() if filename.endswith(".md")]
393 | if not markdown_files:
394 | self._selected_file.pop(context.id, None)
395 | return None
396 |
397 | if context.id not in self._selected_file:
398 | self._selected_file[context.id] = markdown_files[0]
399 |
400 | selected_file_name = self._selected_file[context.id]
401 |
402 | buffer = io.BytesIO()
403 | try:
404 | with drive.open_file(selected_file_name) as file:
405 | buffer.write(file.read())
406 | except FileNotFoundError:
407 | return None
408 |
409 | file_content = buffer.getvalue().decode("utf-8")
410 |
411 | return DocumentFileStateModel(content=file_content, filename=selected_file_name)
412 |
413 | async def write_active_document(self, context: ConversationContext, content: str) -> None:
414 | drive = self._drive_provider(context)
415 | filename = self._selected_file.get(context.id)
416 | if not filename:
417 | raise ValueError("No file selected")
418 |
419 | drive.write(
420 | content=io.BytesIO(content.encode("utf-8")),
421 | filename=filename,
422 | if_exists=IfDriveFileExistsBehavior.OVERWRITE,
423 | content_type="text/markdown",
424 | )
425 |
426 | async def list_documents(self, context: ConversationContext) -> list[DocumentFileStateModel]:
427 | drive = self._drive_provider(context)
428 | markdown_files = [filename for filename in drive.list() if filename.endswith(".md")]
429 |
430 | documents = []
431 | for filename in markdown_files:
432 | buffer = io.BytesIO()
433 | try:
434 | with drive.open_file(filename) as file:
435 | buffer.write(file.read())
436 | except FileNotFoundError:
437 | continue
438 |
439 | file_content = buffer.getvalue().decode("utf-8")
440 | documents.append(DocumentFileStateModel(content=file_content, filename=filename))
441 |
442 | return sorted(documents, key=lambda x: x.filename)
443 |
444 | async def list_document_filenames(self, context: ConversationContext) -> list[str]:
445 | """Returns a list of available markdown document filenames."""
446 | drive = self._drive_provider(context)
447 | markdown_files = [filename for filename in drive.list() if filename.endswith(".md")]
448 | return sorted(markdown_files)
449 |
450 | async def set_active_filename(self, context: ConversationContext, filename: str) -> None:
451 | self._selected_file[context.id] = filename
452 |
453 | await context.send_conversation_state_event(
454 | workbench_model.AssistantStateEvent(
455 | state_id=self._editor.state_id,
456 | event="focus",
457 | state=None,
458 | )
459 | )
460 |
461 | async def get_file_content(self, context: ConversationContext, filename: str) -> str | None:
462 | """
463 | Get the content of a file by filename.
464 |
465 | Args:
466 | context: The conversation context.
467 | filename: The filename of the document to retrieve.
468 |
469 | Returns:
470 | The file content as a string if found, None otherwise.
471 | """
472 | if not await self.is_enabled(context):
473 | return None
474 |
475 | drive = self._drive_provider(context)
476 |
477 | # Check if the file exists in the drive
478 | try:
479 | buffer = io.BytesIO()
480 | with drive.open_file(filename) as file:
481 | buffer.write(file.read())
482 |
483 | return buffer.getvalue().decode("utf-8")
484 | except FileNotFoundError:
485 | # File doesn't exist in the document store
486 | return None
487 |
488 |
489 | @asynccontextmanager
490 | async def lock_document_edits(app: AssistantAppProtocol, context: ConversationContext) -> AsyncGenerator[None, None]:
491 | """
492 | A temporary work-around to call the event handlers directly to communicate the document lock
493 | status to the document inspectors. This circumvents the serialization of event delivery by
494 | calling the event handlers directly.
495 |
496 | It uses an arbitrary event type that the inspector is listening for. The key data is in the
497 | Participant.metadata["document_lock"] field. The rest is unused.
498 | """
499 |
500 | def participant(lock: bool) -> workbench_model.ConversationParticipant:
501 | return workbench_model.ConversationParticipant(
502 | conversation_id=uuid.UUID(context.id),
503 | active_participant=True,
504 | conversation_permission=workbench_model.ConversationPermission.read,
505 | id="",
506 | role=workbench_model.ParticipantRole.assistant,
507 | name="",
508 | status=None,
509 | metadata={
510 | "document_lock": lock,
511 | },
512 | status_updated_timestamp=datetime.datetime.now(),
513 | image=None,
514 | )
515 |
516 | # lock document edits
517 | await app.events.conversation.participant._on_updated_handlers(
518 | False,
519 | context,
520 | None,
521 | participant(True),
522 | )
523 |
524 | try:
525 | yield
526 |
527 | finally:
528 | # unlock the documents
529 | await app.events.conversation.participant._on_updated_handlers(
530 | False,
531 | context,
532 | None,
533 | participant(False),
534 | )
535 |
```
--------------------------------------------------------------------------------
/mcp-servers/mcp-server-filesystem-edit/mcp_server_filesystem_edit/app_handling/powerpoint.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | import re
4 |
5 |
6 | def get_markdown_representation(document) -> str:
7 | """
8 | Return the content of the PowerPoint presentation formatted as a combination of XML and markdown.
9 | """
10 | markdown_text = []
11 |
12 | # Get layout mappings (reverse of the write_markdown layout dictionary)
13 | layout_types = {
14 | 1: "title", # Title Slide
15 | 2: "title_and_content", # Title and Content
16 | 3: "two_content", # Section Header
17 | 33: "section_header", # Section Header
18 | }
19 |
20 | # Process each slide
21 | for slide_index in range(1, document.Slides.Count + 1):
22 | slide = document.Slides(slide_index)
23 |
24 | # Get layout type - slide.Layout is already an integer
25 | layout_type = slide.Layout
26 | layout_name = layout_types.get(layout_type, "title_and_content") # Default if not recognized
27 |
28 | # Start slide tag
29 | markdown_text.append(f'<slide index="{slide_index}" layout="{layout_name}">')
30 |
31 | # Extract title if slide has one
32 | if slide.Shapes.HasTitle:
33 | title_text = slide.Shapes.Title.TextFrame.TextRange.Text
34 | markdown_text.append(f"<title>{title_text}</title>")
35 | else:
36 | markdown_text.append("<title></title>")
37 |
38 | # Process content based on layout type
39 | if layout_name == "title":
40 | # Title slides typically have a subtitle as the second shape
41 | if slide.Shapes.Count >= 2:
42 | subtitle_shape = slide.Shapes(2) # PowerPoint indexing is 1-based
43 | if hasattr(subtitle_shape, "TextFrame"):
44 | subtitle_text = subtitle_shape.TextFrame.TextRange.Text
45 | markdown_text.append(f"<content>{subtitle_text}</content>")
46 | else:
47 | markdown_text.append("<content></content>")
48 | else:
49 | markdown_text.append("<content></content>")
50 |
51 | elif layout_name == "section_header":
52 | # Section headers typically have description text as the second shape
53 | if slide.Shapes.Count >= 2:
54 | description_shape = slide.Shapes(2)
55 | if hasattr(description_shape, "TextFrame"):
56 | description_text = description_shape.TextFrame.TextRange.Text
57 | markdown_text.append(f"<content>{description_text}</content>")
58 | else:
59 | markdown_text.append("<content></content>")
60 | else:
61 | markdown_text.append("<content></content>")
62 |
63 | elif layout_name == "title_and_content":
64 | # Title and content slides have content in the second shape
65 | if slide.Shapes.Count >= 2:
66 | content_shape = slide.Shapes(2)
67 | if hasattr(content_shape, "TextFrame"):
68 | content_text = extract_formatted_content(content_shape.TextFrame)
69 | markdown_text.append(f"<content>\n{content_text}\n</content>")
70 | else:
71 | markdown_text.append("<content></content>")
72 | else:
73 | markdown_text.append("<content></content>")
74 |
75 | elif layout_name == "two_content":
76 | # Two content slides have content in second and third shapes
77 | # First content
78 | if slide.Shapes.Count >= 2:
79 | left_content_shape = slide.Shapes(2)
80 | if hasattr(left_content_shape, "TextFrame"):
81 | left_content_text = extract_formatted_content(left_content_shape.TextFrame)
82 | markdown_text.append(f"<content>\n{left_content_text}\n</content>")
83 | else:
84 | markdown_text.append("<content></content>")
85 | else:
86 | markdown_text.append("<content></content>")
87 |
88 | # Second content
89 | if slide.Shapes.Count >= 3:
90 | right_content_shape = slide.Shapes(3)
91 | if hasattr(right_content_shape, "TextFrame"):
92 | right_content_text = extract_formatted_content(right_content_shape.TextFrame)
93 | markdown_text.append(f"<content>\n{right_content_text}\n</content>")
94 | else:
95 | markdown_text.append("<content></content>")
96 | else:
97 | markdown_text.append("<content></content>")
98 |
99 | # Close slide tag
100 | markdown_text.append("</slide>\n")
101 |
102 | return "\n".join(markdown_text)
103 |
104 |
105 | def extract_formatted_content(text_frame) -> str:
106 | markdown_lines = []
107 | for para_index in range(1, text_frame.TextRange.Paragraphs().Count + 1):
108 | para = text_frame.TextRange.Paragraphs(para_index)
109 |
110 | # Skip empty paragraphs
111 | if not para.Text.strip():
112 | markdown_lines.append("")
113 | continue
114 |
115 | # Check if paragraph is a heading based on font size
116 | font_size = para.Font.Size
117 | is_bold = para.Font.Bold
118 | line = ""
119 |
120 | # Handle bulleted lists
121 | if para.ParagraphFormat.Bullet.Type == 1: # Bullet
122 | line = "- "
123 | # Handle numbered lists
124 | elif para.ParagraphFormat.Bullet.Type == 2: # Numbered
125 | line = "1. " # We'll use 1. for all items since PowerPoint maintains the actual numbering
126 |
127 | # Handle headers (larger font with bold)
128 | elif font_size >= 20 and is_bold:
129 | # Inverse of font size calculation in add_formatted_content
130 | heading_level = int((30 - font_size) // 2)
131 | line = "#" * heading_level + " "
132 |
133 | # Process text with formatting
134 | line += extract_text_with_formatting(para)
135 | markdown_lines.append(line)
136 |
137 | return "\n".join(markdown_lines)
138 |
139 |
140 | def extract_text_with_formatting(text_range) -> str:
141 | # For simple cases where there's no mixed formatting
142 | if text_range.Characters().Count <= 1:
143 | return text_range.Text
144 |
145 | # For performance, first check if there's any formatting at all
146 | has_formatting = False
147 | for i in range(1, text_range.Characters().Count + 1):
148 | char = text_range.Characters(i)
149 | if char.Font.Bold or char.Font.Italic:
150 | has_formatting = True
151 | break
152 |
153 | # If no formatting, return the plain text
154 | if not has_formatting:
155 | return text_range.Text
156 |
157 | formatted_text = []
158 | current_run = {"text": "", "bold": False, "italic": False}
159 | for i in range(1, text_range.Characters().Count + 1):
160 | char = text_range.Characters(i)
161 | char_text = char.Text
162 |
163 | # Skip null characters or carriage returns
164 | if not char_text or ord(char_text) == 13:
165 | continue
166 |
167 | is_bold = char.Font.Bold
168 | is_italic = char.Font.Italic
169 | # If formatting changed, start a new run
170 | if is_bold != current_run["bold"] or is_italic != current_run["italic"]:
171 | # Finish the previous run if it exists
172 | if current_run["text"]:
173 | if current_run["bold"] and current_run["italic"]:
174 | formatted_text.append(f"***{current_run['text']}***")
175 | elif current_run["bold"]:
176 | formatted_text.append(f"**{current_run['text']}**")
177 | elif current_run["italic"]:
178 | formatted_text.append(f"*{current_run['text']}*")
179 | else:
180 | formatted_text.append(current_run["text"])
181 |
182 | current_run = {"text": char_text, "bold": is_bold, "italic": is_italic}
183 | else:
184 | current_run["text"] += char_text
185 |
186 | # Process the final run
187 | if current_run["text"]:
188 | if current_run["bold"] and current_run["italic"]:
189 | formatted_text.append(f"***{current_run['text']}***")
190 | elif current_run["bold"]:
191 | formatted_text.append(f"**{current_run['text']}**")
192 | elif current_run["italic"]:
193 | formatted_text.append(f"*{current_run['text']}*")
194 | else:
195 | formatted_text.append(current_run["text"])
196 |
197 | return "".join(formatted_text)
198 |
199 |
200 | def write_markdown(document, markdown_text: str) -> None:
201 | """
202 | Converts markdown text to PowerPoint slides with themes.
203 | Uses a structured format with <slide> tags to define slides.
204 |
205 | Format:
206 | <slide index=1 layout=<"title"|"section header"|"two content"|"title and content">
207 | <title>Title text</title>
208 | <content>Markdown content</content>
209 | <content>Second Markdown content - use only if two content is chosen </content>
210 | </slide>
211 | <slide index=2 layout=<"title"|"section header"|"two content"|"title and content">
212 | ...
213 | </slide>
214 | ...
215 |
216 | Other syntax and settings:
217 | - Heading has no Markdown formatting applied
218 | - Content is treated as Markdown, with paragraphs, H2, H3, bold, italics, numbered lists, and bullet points supported.
219 | - Second content is only used if layout is "two content" and ignored otherwise.
220 | """
221 | # Clear all slides and sections
222 | try:
223 | document.Slides.Range().Delete()
224 | if document.SectionProperties.Count > 0:
225 | for i in range(document.SectionProperties.Count, 0, -1):
226 | document.SectionProperties.Delete(i, True)
227 | except Exception as e:
228 | print(f"Error deleting slides: {e}")
229 |
230 | # Create mappings to PowerPoint slide layouts types
231 | layouts = {
232 | "title": 1, # Title Slide
233 | "title_and_content": 2, # Title and Content
234 | "two_content": 3, # Two Content
235 | "section_header": 33, # Section Header
236 | }
237 |
238 | slide_pattern = re.compile(r'<slide\s+index=(?:"?(\d+)"?)\s+layout="([^"]+)">(.*?)</slide>', re.DOTALL)
239 | title_pattern = re.compile(r"<title>(.*?)</title>", re.DOTALL)
240 | content_pattern = re.compile(r"<content>(.*?)</content>", re.DOTALL)
241 |
242 | slides = slide_pattern.findall(markdown_text)
243 | for _, layout_name, slide_content in slides:
244 | layout_name_typed = layout_name.lower()
245 | # Use default layout if not recognized
246 | if layout_name_typed not in layouts:
247 | layout_name_typed = "title_and_content"
248 |
249 | # Extract title and content
250 | title_match = title_pattern.search(slide_content)
251 | title_text = title_match.group(1).strip() if title_match else ""
252 |
253 | # Extract content sections
254 | content_matches = content_pattern.findall(slide_content)
255 | content_text = content_matches[0].strip() if content_matches else ""
256 | second_content_text = content_matches[1].strip() if len(content_matches) > 1 else ""
257 |
258 | # Create a new slide with the specified layout
259 | slide_layout_index = layouts[layout_name_typed]
260 | slide = document.Slides.Add(document.Slides.Count + 1, slide_layout_index)
261 |
262 | # Add title if it exists
263 | if title_text and slide.Shapes.HasTitle:
264 | slide.Shapes.Title.TextFrame.TextRange.Text = title_text
265 |
266 | # Process content based on layout type
267 | if layout_name_typed == "title":
268 | if content_text and slide.Shapes.Count >= 2:
269 | subtitle_shape = slide.Shapes[1]
270 | if hasattr(subtitle_shape, "TextFrame"):
271 | add_formatted_content(subtitle_shape.TextFrame, content_text)
272 |
273 | elif layout_name_typed == "section_header":
274 | if content_text and slide.Shapes.Count >= 2:
275 | description_shape = slide.Shapes[1]
276 | if hasattr(description_shape, "TextFrame"):
277 | add_formatted_content(description_shape.TextFrame, content_text)
278 |
279 | elif layout_name_typed == "title_and_content":
280 | if content_text and slide.Shapes.Count >= 2:
281 | content_shape = slide.Shapes[1]
282 | if hasattr(content_shape, "TextFrame"):
283 | add_formatted_content(content_shape.TextFrame, content_text)
284 |
285 | elif layout_name_typed == "two_content":
286 | if content_text and slide.Shapes.Count >= 2:
287 | left_content_shape = slide.Shapes[1]
288 | if hasattr(left_content_shape, "TextFrame"):
289 | add_formatted_content(left_content_shape.TextFrame, content_text)
290 |
291 | if second_content_text and slide.Shapes.Count >= 3:
292 | right_content_shape = slide.Shapes[2]
293 | if hasattr(right_content_shape, "TextFrame"):
294 | add_formatted_content(right_content_shape.TextFrame, second_content_text)
295 |
296 |
297 | def add_formatted_content(text_frame, content: str) -> None:
298 | """
299 | Adds formatted content to a PowerPoint text frame.
300 | Supports Markdown formatting including headers, lists, bold, and italic.
301 |
302 | Args:
303 | text_frame: PowerPoint TextFrame object
304 | content: Markdown-formatted text to add
305 | """
306 | # Clear existing text
307 | text_frame.TextRange.Text = ""
308 | text_range = text_frame.TextRange
309 | text_range.ParagraphFormat.Bullet.Type = 0
310 |
311 | # Split content into paragraphs
312 | paragraphs = content.split("\n")
313 |
314 | # Track if we're in a list and what type
315 | in_bulleted_list = False
316 | in_numbered_list = False
317 | for i, paragraph in enumerate(paragraphs):
318 | # Skip empty paragraphs and clear formatting
319 | if not paragraph.strip():
320 | if i < len(paragraphs) - 1:
321 | text_range.InsertAfter("\r")
322 | continue
323 |
324 | # Insert paragraph break for non-first paragraphs
325 | if i > 0:
326 | # Use newline for paragraph break in PowerPoint
327 | text_range.InsertAfter("\r")
328 |
329 | # Access the end of what was just inserted
330 | text_length = len(text_frame.TextRange.Text)
331 | text_range = text_frame.TextRange.Characters(text_length + 1)
332 |
333 | # Check for headers (## Header)
334 | if paragraph.strip().startswith("#"):
335 | # Clear any previous formatting for headers
336 | text_range.ParagraphFormat.Bullet.Type = 0
337 | in_bulleted_list = False
338 | in_numbered_list = False
339 |
340 | # Calculate header level
341 | header_level = 0
342 | for char in paragraph:
343 | if char == "#":
344 | header_level += 1
345 | else:
346 | break
347 |
348 | header_text = paragraph[header_level:].strip()
349 |
350 | # Set header formatting before inserting text
351 | text_range.Font.Bold = True
352 | text_range.Font.Size = 30 - (header_level * 2) if header_level > 1 else 30
353 |
354 | format_and_insert_text(text_range, header_text)
355 | continue
356 |
357 | # Check for bullet points
358 | if paragraph.strip().startswith(("- ", "* ")):
359 | # Extract content after bullet
360 | bullet_text = paragraph.strip()[2:].strip()
361 |
362 | # Apply bullet formatting to the paragraph
363 | text_range.ParagraphFormat.Bullet.Type = 1
364 |
365 | # Reset text formatting for bullet content
366 | text_range.Font.Size = 18
367 | text_range.Font.Bold = False
368 | text_range.Font.Italic = False
369 |
370 | in_bulleted_list = True
371 | in_numbered_list = False
372 |
373 | format_and_insert_text(text_range, bullet_text)
374 | continue
375 |
376 | # Check for numbered lists
377 | numbered_match = re.match(r"^\s*(\d+)[\.\)]\s+(.*)", paragraph.strip())
378 | if numbered_match:
379 | # Extract content after number
380 | number_text = numbered_match.group(2).strip()
381 |
382 | # Apply numbered list formatting to the paragraph
383 | text_range.ParagraphFormat.Bullet.Type = 2
384 |
385 | # Reset text formatting for numbered content
386 | text_range.Font.Size = 18
387 | text_range.Font.Bold = False
388 | text_range.Font.Italic = False
389 |
390 | in_numbered_list = True
391 | in_bulleted_list = False
392 |
393 | format_and_insert_text(text_range, number_text)
394 | continue
395 |
396 | # For regular paragraphs (not headers or list items)
397 | if in_bulleted_list or in_numbered_list:
398 | text_range.ParagraphFormat.Bullet.Type = 0
399 | in_bulleted_list = False
400 | in_numbered_list = False
401 |
402 | text_range.Font.Size = 18
403 | text_range.Font.Bold = False
404 | text_range.Font.Italic = False
405 |
406 | format_and_insert_text(text_range, paragraph)
407 |
408 |
409 | def format_and_insert_text(text_range, text: str) -> None:
410 | """
411 | Formats and inserts text with markdown-style formatting.
412 | Supports bold, italic, and bold+italic.
413 | """
414 | # First parse the text into formatted segments
415 | segments = []
416 | i = 0
417 | while i < len(text):
418 | # Bold+Italic (***text***)
419 | if i + 2 < len(text) and text[i : i + 3] == "***" and "***" in text[i + 3 :]:
420 | end_pos = text.find("***", i + 3)
421 | if end_pos != -1:
422 | segments.append(("bold_italic", text[i + 3 : end_pos]))
423 | i = end_pos + 3
424 | continue
425 |
426 | # Bold (**text**)
427 | elif i + 1 < len(text) and text[i : i + 2] == "**" and "**" in text[i + 2 :]:
428 | end_pos = text.find("**", i + 2)
429 | if end_pos != -1:
430 | segments.append(("bold", text[i + 2 : end_pos]))
431 | i = end_pos + 2
432 | continue
433 |
434 | # Italic (*text*)
435 | elif text[i] == "*" and i + 1 < len(text) and text[i + 1] != "*" and "*" in text[i + 1 :]:
436 | end_pos = text.find("*", i + 1)
437 | if end_pos != -1:
438 | segments.append(("italic", text[i + 1 : end_pos]))
439 | i = end_pos + 1
440 | continue
441 |
442 | # Find the next special marker or end of string
443 | next_marker = len(text)
444 | for marker in ["***", "**", "*"]:
445 | pos = text.find(marker, i)
446 | if pos != -1 and pos < next_marker:
447 | next_marker = pos
448 |
449 | # Add plain text segment
450 | if next_marker == len(text):
451 | segments.append(("plain", text[i:]))
452 | break
453 | else:
454 | segments.append(("plain", text[i:next_marker]))
455 | i = next_marker
456 |
457 | # If no formatting needed, just insert the whole text
458 | if all(segment[0] == "plain" for segment in segments):
459 | text_range.InsertAfter("".join(segment[1] for segment in segments))
460 | return
461 |
462 | # Insert all text first as plain text
463 | full_text = "".join(segment[1] for segment in segments)
464 | start_pos = len(text_range.Text)
465 | text_range.InsertAfter(full_text)
466 |
467 | # Now apply formatting to specific parts
468 | current_pos = start_pos
469 | for format_type, content in segments:
470 | if not content or format_type == "plain":
471 | current_pos += len(content)
472 | continue
473 |
474 | # Select the range to format
475 | format_range = text_range.Characters(
476 | current_pos + 1, # Start (1-indexed)
477 | len(content), # Length
478 | )
479 |
480 | # Apply formatting
481 | if format_type in ("bold", "bold_italic"):
482 | format_range.Font.Bold = True
483 |
484 | if format_type in ("italic", "bold_italic"):
485 | format_range.Font.Italic = True
486 |
487 | current_pos += len(content)
488 |
489 | # Reset the text_range to point to the end for future insertions
490 | text_length = len(text_range.Text)
491 | text_range = text_range.Characters(text_length)
492 |
```
--------------------------------------------------------------------------------
/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_service.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import logging
3 | import random
4 | from abc import ABC, abstractmethod
5 | from contextlib import AsyncExitStack, asynccontextmanager
6 | from typing import (
7 | IO,
8 | Annotated,
9 | AsyncContextManager,
10 | AsyncGenerator,
11 | AsyncIterator,
12 | Callable,
13 | NoReturn,
14 | Optional,
15 | )
16 |
17 | import asgi_correlation_id
18 | import backoff
19 | import backoff.types
20 | import httpx
21 | import semantic_workbench_api_model.workbench_service_client
22 | from fastapi import (
23 | FastAPI,
24 | File,
25 | Form,
26 | HTTPException,
27 | Request,
28 | Response,
29 | UploadFile,
30 | status,
31 | )
32 | from fastapi.encoders import jsonable_encoder
33 | from fastapi.exception_handlers import http_exception_handler
34 | from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
35 | from pydantic import BaseModel, HttpUrl, ValidationError
36 | from semantic_workbench_api_model import (
37 | assistant_model,
38 | workbench_model,
39 | workbench_service_client,
40 | )
41 | from starlette.exceptions import HTTPException as StarletteHTTPException
42 |
43 | from . import auth, settings
44 | from .logging_config import log_request_middleware
45 |
46 | logger = logging.getLogger(__name__)
47 |
48 |
49 | def _backoff_success_handler(details: backoff.types.Details) -> None:
50 | if details["tries"] == 1:
51 | return
52 | logger.info(
53 | "Success after backoff %s(...); tries: %d, elapsed: %.1fs",
54 | details["target"].__name__,
55 | details["tries"],
56 | details["elapsed"],
57 | )
58 |
59 |
60 | class FastAPIAssistantService(ABC):
61 | """
62 | Base class for implementations of assistant services using FastAPI.
63 | """
64 |
65 | def __init__(
66 | self,
67 | service_id: str,
68 | service_name: str,
69 | service_description: str,
70 | register_lifespan_handler: Callable[[Callable[[], AsyncContextManager[None]]], None],
71 | ) -> None:
72 | self._service_id = service_id
73 | self._service_name = service_name
74 | self._service_description = service_description
75 | self._workbench_httpx_client = httpx.AsyncClient(
76 | transport=semantic_workbench_api_model.workbench_service_client.httpx_transport_factory(),
77 | timeout=httpx.Timeout(5.0, connect=10.0, read=60.0),
78 | base_url=str(settings.workbench_service_url),
79 | )
80 |
81 | @asynccontextmanager
82 | async def lifespan() -> AsyncIterator[None]:
83 | logger.info(
84 | "connecting to semantic-workbench-service; workbench_service_url: %s, assistant_service_id: %s, callback_url: %s",
85 | settings.workbench_service_url,
86 | self.service_id,
87 | settings.callback_url,
88 | )
89 |
90 | service_client = self.workbench_client.for_service()
91 | # start periodic pings to workbench
92 | ping_task = asyncio.create_task(
93 | self._periodically_ping_semantic_workbench(service_client), name="ping-workbench"
94 | )
95 |
96 | try:
97 | yield
98 |
99 | finally:
100 | ping_task.cancel()
101 | try:
102 | await ping_task
103 | except asyncio.CancelledError:
104 | pass
105 |
106 | register_lifespan_handler(lifespan)
107 |
108 | async def _periodically_ping_semantic_workbench(
109 | self, client: workbench_service_client.AssistantServiceAPIClient
110 | ) -> NoReturn:
111 | while True:
112 | try:
113 | try:
114 | await self._ping_semantic_workbench(client)
115 | except httpx.HTTPError:
116 | logger.exception("ping to workbench failed")
117 |
118 | jitter = random.uniform(0, settings.workbench_service_ping_interval_seconds / 2.0)
119 | await asyncio.sleep(settings.workbench_service_ping_interval_seconds + jitter)
120 |
121 | except Exception:
122 | logger.exception("unexpected error in ping loop")
123 |
124 | @backoff.on_exception(
125 | backoff.expo,
126 | httpx.HTTPError,
127 | max_time=30,
128 | logger=logger,
129 | on_success=_backoff_success_handler,
130 | )
131 | async def _ping_semantic_workbench(self, client: workbench_service_client.AssistantServiceAPIClient) -> None:
132 | try:
133 | await client.update_registration_url(
134 | assistant_service_id=self.service_id,
135 | update=workbench_model.UpdateAssistantServiceRegistrationUrl(
136 | name=self.service_name,
137 | description=self.service_description,
138 | url=HttpUrl(settings.callback_url),
139 | online_expires_in_seconds=settings.workbench_service_ping_interval_seconds * 3.5,
140 | ),
141 | )
142 |
143 | except httpx.HTTPStatusError as e:
144 | # log additional information for common error cases
145 | match e.response.status_code:
146 | case 401:
147 | logger.warning(
148 | "authentication failed with semantic-workbench service, configured assistant_service_id and/or"
149 | " workbench_service_api_key are incorrect; workbench_service_url: %s,"
150 | " assistant_service_id: %s, callback_url: %s",
151 | settings.workbench_service_url,
152 | self.service_id,
153 | settings.callback_url,
154 | )
155 | case 404:
156 | logger.warning(
157 | "configured assistant_service_id does not exist in the semantic-workbench-service;"
158 | " workbench_service_url: %s, assistant_service_id: %s, callback_url: %s",
159 | settings.workbench_service_url,
160 | self.service_id,
161 | settings.callback_url,
162 | )
163 | raise
164 |
165 | @property
166 | def service_id(self) -> str:
167 | return settings.assistant_service_id if settings.assistant_service_id is not None else self._service_id
168 |
169 | @property
170 | def service_name(self) -> str:
171 | return settings.assistant_service_name if settings.assistant_service_name is not None else self._service_name
172 |
173 | @property
174 | def service_description(self) -> str:
175 | return (
176 | settings.assistant_service_description
177 | if settings.assistant_service_description is not None
178 | else self._service_description
179 | )
180 |
181 | @property
182 | def workbench_client(self) -> workbench_service_client.WorkbenchServiceClientBuilder:
183 | return workbench_service_client.WorkbenchServiceClientBuilder(
184 | assistant_service_id=self.service_id,
185 | api_key=settings.workbench_service_api_key,
186 | httpx_client=self._workbench_httpx_client,
187 | )
188 |
189 | @abstractmethod
190 | async def get_service_info(self) -> assistant_model.ServiceInfoModel:
191 | pass
192 |
193 | @abstractmethod
194 | async def put_assistant(
195 | self,
196 | assistant_id: str,
197 | assistant: assistant_model.AssistantPutRequestModel,
198 | from_export: Optional[IO[bytes]] = None,
199 | ) -> assistant_model.AssistantResponseModel:
200 | pass
201 |
202 | @abstractmethod
203 | async def export_assistant_data(
204 | self, assistant_id: str
205 | ) -> StreamingResponse | FileResponse | JSONResponse | BaseModel:
206 | pass
207 |
208 | @abstractmethod
209 | async def get_assistant(self, assistant_id: str) -> assistant_model.AssistantResponseModel:
210 | pass
211 |
212 | @abstractmethod
213 | async def delete_assistant(self, assistant_id: str) -> None:
214 | pass
215 |
216 | @abstractmethod
217 | async def get_config(self, assistant_id: str) -> assistant_model.ConfigResponseModel:
218 | pass
219 |
220 | @abstractmethod
221 | async def put_config(
222 | self, assistant_id: str, updated_config: assistant_model.ConfigPutRequestModel
223 | ) -> assistant_model.ConfigResponseModel:
224 | pass
225 |
226 | @abstractmethod
227 | async def put_conversation(
228 | self,
229 | assistant_id: str,
230 | conversation_id: str,
231 | conversation: assistant_model.ConversationPutRequestModel,
232 | from_export: Optional[IO[bytes]] = None,
233 | ) -> assistant_model.ConversationResponseModel:
234 | pass
235 |
236 | @abstractmethod
237 | async def export_conversation_data(
238 | self,
239 | assistant_id: str,
240 | conversation_id: str,
241 | ) -> StreamingResponse | FileResponse | JSONResponse | BaseModel:
242 | pass
243 |
244 | @abstractmethod
245 | async def get_conversation(
246 | self, assistant_id: str, conversation_id: str
247 | ) -> assistant_model.ConversationResponseModel:
248 | pass
249 |
250 | @abstractmethod
251 | async def delete_conversation(self, assistant_id: str, conversation_id: str) -> None:
252 | pass
253 |
254 | @abstractmethod
255 | async def post_conversation_event(
256 | self,
257 | assistant_id: str,
258 | conversation_id: str,
259 | event: workbench_model.ConversationEvent,
260 | ) -> None:
261 | pass
262 |
263 | @abstractmethod
264 | async def get_conversation_state_descriptions(
265 | self, assistant_id: str, conversation_id: str
266 | ) -> assistant_model.StateDescriptionListResponseModel:
267 | pass
268 |
269 | @abstractmethod
270 | async def get_conversation_state(
271 | self, assistant_id: str, conversation_id: str, state_id: str
272 | ) -> assistant_model.StateResponseModel:
273 | pass
274 |
275 | @abstractmethod
276 | async def put_conversation_state(
277 | self,
278 | assistant_id: str,
279 | conversation_id: str,
280 | state_id: str,
281 | updated_state: assistant_model.StatePutRequestModel,
282 | ) -> assistant_model.StateResponseModel:
283 | pass
284 |
285 |
286 | def _assistant_service_api(
287 | app: FastAPI,
288 | service: FastAPIAssistantService,
289 | enable_auth_middleware: bool = True,
290 | ):
291 | """
292 | Implements API for AssistantService, forwarding requests to AssistantService.
293 | """
294 |
295 | if enable_auth_middleware:
296 | app.add_middleware(
297 | middleware_class=auth.AuthMiddleware,
298 | exclude_methods={"OPTIONS"},
299 | exclude_paths=set(settings.anonymous_paths),
300 | )
301 | app.add_middleware(asgi_correlation_id.CorrelationIdMiddleware)
302 | app.middleware("http")(log_request_middleware())
303 |
304 | @app.exception_handler(StarletteHTTPException)
305 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException) -> Response:
306 | if 500 <= exc.status_code < 600:
307 | logger.exception(
308 | "exception in request handler; method: %s, path: %s", request.method, request.url.path, exc_info=exc
309 | )
310 | return await http_exception_handler(request, exc)
311 |
312 | @app.get("/", description="Get the description of the assistant service")
313 | async def get_service_description(response: Response) -> assistant_model.ServiceInfoModel:
314 | response.headers["Cache-Control"] = "max-age=600"
315 | return await service.get_service_info()
316 |
317 | @app.put(
318 | "/{assistant_id}",
319 | description=(
320 | "Connect an assistant to the workbench, optionally providing exported-data to restore the assistant"
321 | ),
322 | )
323 | async def put_assistant(
324 | assistant_id: str,
325 | assistant_json: Annotated[str, Form(alias="assistant")],
326 | from_export: Annotated[Optional[UploadFile], File(alias="from_export")] = None,
327 | ) -> assistant_model.AssistantResponseModel:
328 | try:
329 | assistant_request = assistant_model.AssistantPutRequestModel.model_validate_json(assistant_json)
330 | except ValidationError as e:
331 | raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=e.errors())
332 |
333 | if from_export:
334 | return await service.put_assistant(assistant_id, assistant_request, from_export.file)
335 |
336 | return await service.put_assistant(assistant_id, assistant_request)
337 |
338 | @app.get(
339 | "/{assistant_id}",
340 | description="Get an assistant",
341 | )
342 | async def get_assistant(assistant_id: str) -> assistant_model.AssistantResponseModel:
343 | return await service.get_assistant(assistant_id)
344 |
345 | @app.delete(
346 | "/{assistant_id}",
347 | description="Delete an assistant",
348 | )
349 | async def delete_assistant(assistant_id: str) -> None:
350 | return await service.delete_assistant(assistant_id)
351 |
352 | @app.get(
353 | "/{assistant_id}/export-data",
354 | description="Export all data for this assistant",
355 | )
356 | async def export_assistant_data(assistant_id: str) -> Response:
357 | response = await service.export_assistant_data(assistant_id)
358 | match response:
359 | case StreamingResponse() | FileResponse() | JSONResponse():
360 | return response
361 | case BaseModel():
362 | return JSONResponse(jsonable_encoder(response))
363 | case _:
364 | raise TypeError(f"Unexpected response type {type(response)}")
365 |
366 | @app.get(
367 | "/{assistant_id}/config",
368 | description="Get config for this assistant",
369 | )
370 | async def get_config(assistant_id: str) -> assistant_model.ConfigResponseModel:
371 | return await service.get_config(assistant_id)
372 |
373 | @app.put(
374 | "/{assistant_id}/config",
375 | description="Set config for this assistant",
376 | )
377 | async def put_config(
378 | assistant_id: str, updated_config: assistant_model.ConfigPutRequestModel
379 | ) -> assistant_model.ConfigResponseModel:
380 | return await service.put_config(assistant_id, updated_config=updated_config)
381 |
382 | @app.put(
383 | "/{assistant_id}/conversations/{conversation_id}",
384 | description=(
385 | "Join an assistant to a workbench conversation, optionally"
386 | " providing exported-data to restore the conversation"
387 | ),
388 | )
389 | async def put_conversation(
390 | assistant_id: str,
391 | conversation_id: str,
392 | conversation_json: Annotated[str, Form(alias="conversation")],
393 | from_export: Annotated[Optional[UploadFile], File(alias="from_export")] = None,
394 | ) -> assistant_model.ConversationResponseModel:
395 | try:
396 | conversation = assistant_model.ConversationPutRequestModel.model_validate_json(conversation_json)
397 | except ValidationError as e:
398 | raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=e.errors())
399 |
400 | if from_export:
401 | return await service.put_conversation(assistant_id, conversation_id, conversation, from_export.file)
402 |
403 | return await service.put_conversation(assistant_id, conversation_id, conversation)
404 |
405 | @app.get(
406 | "/{assistant_id}/conversations/{conversation_id}",
407 | description="Get the status of a conversation",
408 | )
409 | async def get_conversation(assistant_id: str, conversation_id: str) -> assistant_model.ConversationResponseModel:
410 | return await service.get_conversation(assistant_id, conversation_id)
411 |
412 | @app.delete(
413 | "/{assistant_id}/conversations/{conversation_id}",
414 | description="Delete a conversation",
415 | )
416 | async def delete_conversation(assistant_id: str, conversation_id: str) -> None:
417 | return await service.delete_conversation(assistant_id, conversation_id)
418 |
419 | @app.get(
420 | "/{assistant_id}/conversations/{conversation_id}/export-data",
421 | description="Export all data for a conversation",
422 | )
423 | async def export_conversation_data(assistant_id: str, conversation_id: str) -> Response:
424 | response = await service.export_conversation_data(assistant_id=assistant_id, conversation_id=conversation_id)
425 | match response:
426 | case StreamingResponse():
427 | return response
428 | case FileResponse():
429 | return response
430 | case JSONResponse():
431 | return response
432 | case BaseModel():
433 | return JSONResponse(jsonable_encoder(response))
434 | case _:
435 | raise TypeError(f"Unexpected response type {type(response)}")
436 |
437 | @app.post(
438 | "/{assistant_id}/conversations/{conversation_id}/events",
439 | description="Notify assistant of an event in the conversation",
440 | status_code=status.HTTP_204_NO_CONTENT,
441 | )
442 | async def post_conversation_event(
443 | assistant_id: str,
444 | conversation_id: str,
445 | event: workbench_model.ConversationEvent,
446 | ) -> None:
447 | return await service.post_conversation_event(assistant_id, conversation_id, event)
448 |
449 | @app.get(
450 | "/{assistant_id}/conversations/{conversation_id}/states",
451 | description="Get the descriptions of the states available for a conversation",
452 | )
453 | async def get_conversation_state_descriptions(
454 | assistant_id: str, conversation_id: str
455 | ) -> assistant_model.StateDescriptionListResponseModel:
456 | return await service.get_conversation_state_descriptions(assistant_id, conversation_id)
457 |
458 | @app.get(
459 | "/{assistant_id}/conversations/{conversation_id}/states/{state_id}",
460 | description="Get a specific state by id for a conversation",
461 | )
462 | async def get_conversation_state(
463 | assistant_id: str, conversation_id: str, state_id: str
464 | ) -> assistant_model.StateResponseModel:
465 | return await service.get_conversation_state(assistant_id, conversation_id, state_id)
466 |
467 | @app.put(
468 | "/{assistant_id}/conversations/{conversation_id}/states/{state_id}",
469 | description="Update a specific state by id for a conversation",
470 | )
471 | async def put_conversation_state(
472 | assistant_id: str,
473 | conversation_id: str,
474 | state_id: str,
475 | updated_state: assistant_model.StatePutRequestModel,
476 | ) -> assistant_model.StateResponseModel:
477 | return await service.put_conversation_state(assistant_id, conversation_id, state_id, updated_state)
478 |
479 |
480 | logger = logging.getLogger(__name__)
481 |
482 |
483 | class FastAPILifespan:
484 | def __init__(self) -> None:
485 | self._lifecycle_handlers: list[Callable[[], AsyncContextManager[None]]] = []
486 |
487 | def register_handler(self, handler: Callable[[], AsyncContextManager[None]]) -> None:
488 | self._lifecycle_handlers.append(handler)
489 |
490 | @asynccontextmanager
491 | async def lifespan(self, app: FastAPI) -> AsyncGenerator[None, None]:
492 | async with AsyncExitStack() as stack:
493 | logger.debug("app lifespan starting up; title: %s, version: %s", app.title, app.version)
494 |
495 | for handler in self._lifecycle_handlers:
496 | await stack.enter_async_context(handler())
497 |
498 | logger.info("app lifespan started; title: %s, version: %s", app.title, app.version)
499 |
500 | try:
501 | yield
502 | finally:
503 | logger.debug("app lifespan shutting down; title: %s, version: %s", app.title, app.version)
504 |
505 | logger.info("app lifespan shut down; title: %s, version: %s", app.title, app.version)
506 |
507 |
508 | def create_app(
509 | factory: Callable[[FastAPILifespan], FastAPIAssistantService],
510 | enable_auth_middleware: bool = True,
511 | ) -> FastAPI:
512 | """
513 | Create a FastAPI app for an AssistantService.
514 | """
515 | lifespan = FastAPILifespan()
516 | svc = factory(lifespan)
517 | app = FastAPI(
518 | lifespan=lifespan.lifespan,
519 | title=svc.service_name,
520 | description=svc.service_description,
521 | # extra is used to store metadata about the service
522 | assistant_service_id=svc.service_id,
523 | )
524 | _assistant_service_api(app, svc, enable_auth_middleware)
525 | return app
526 |
```