This is page 89 of 114. Use http://codebase.md/microsoft/semanticworkbench?lines=false&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
--------------------------------------------------------------------------------
/workbench-service/semantic_workbench_service/service.py:
--------------------------------------------------------------------------------
```python
import asyncio
import contextlib
import datetime
import json
import logging
import urllib.parse
import uuid
from collections import defaultdict
from contextlib import asynccontextmanager
from typing import (
Annotated,
Any,
AsyncContextManager,
AsyncIterator,
Callable,
NoReturn,
)
import asgi_correlation_id
import starlette.background
from asgi_correlation_id import CorrelationIdMiddleware
from fastapi import (
BackgroundTasks,
FastAPI,
File,
Form,
HTTPException,
Query,
Request,
Response,
UploadFile,
status,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from semantic_workbench_api_model.assistant_model import (
ConfigPutRequestModel,
ConfigResponseModel,
ServiceInfoModel,
StateDescriptionListResponseModel,
StatePutRequestModel,
StateResponseModel,
)
from semantic_workbench_api_model.workbench_model import (
Assistant,
AssistantList,
AssistantServiceInfoList,
AssistantServiceRegistration,
AssistantServiceRegistrationList,
AssistantStateEvent,
Conversation,
ConversationEvent,
ConversationEventType,
ConversationImportResult,
ConversationList,
ConversationMessage,
ConversationMessageDebug,
ConversationMessageList,
ConversationParticipant,
ConversationParticipantList,
ConversationShare,
ConversationShareList,
ConversationShareRedemption,
ConversationShareRedemptionList,
FileList,
FileVersions,
MessageType,
NewAssistant,
NewAssistantServiceRegistration,
NewConversation,
NewConversationMessage,
NewConversationShare,
ParticipantRole,
UpdateAssistant,
UpdateAssistantServiceRegistration,
UpdateAssistantServiceRegistrationUrl,
UpdateConversation,
UpdateFile,
UpdateParticipant,
UpdateUser,
User,
UserList,
)
from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette import EventSourceResponse, ServerSentEvent
from semantic_workbench_service import azure_speech
from semantic_workbench_service.logging_config import log_request_middleware
from . import assistant_api_key, auth, controller, db, files, middleware, settings
from .event import ConversationEventQueueItem
logger = logging.getLogger(__name__)
def init(
app: FastAPI,
register_lifespan_handler: Callable[[Callable[[], AsyncContextManager[None]]], None],
) -> None:
api_key_store = assistant_api_key.get_store()
stop_signal: asyncio.Event = asyncio.Event()
conversation_sse_queues_lock = asyncio.Lock()
conversation_sse_queues: dict[uuid.UUID, set[asyncio.Queue[ConversationEvent]]] = defaultdict(set)
user_sse_queues_lock = asyncio.Lock()
user_sse_queues: dict[str, set[asyncio.Queue[ConversationEvent]]] = defaultdict(set)
assistant_event_queues: dict[uuid.UUID, asyncio.Queue[ConversationEvent]] = {}
background_tasks: set[asyncio.Task] = set()
def _controller_get_session() -> AsyncContextManager[AsyncSession]:
return db.create_session(app.state.db_engine)
async def _forward_events_to_assistant(
assistant_id: uuid.UUID, event_queue: asyncio.Queue[ConversationEvent]
) -> NoReturn:
while True:
try:
event = await event_queue.get()
event_queue.task_done()
asgi_correlation_id.correlation_id.set(event.correlation_id)
start_time = datetime.datetime.now(datetime.UTC)
await assistant_controller.forward_event_to_assistant(assistant_id=assistant_id, event=event)
end_time = datetime.datetime.now(datetime.UTC)
logger.debug(
"forwarded event to assistant; assistant_id: %s, conversation_id: %s, event_id: %s,"
" duration: %s, time since event: %s",
assistant_id,
event.conversation_id,
event.id,
end_time - start_time,
end_time - event.timestamp,
)
except Exception:
logger.exception("exception in _forward_events_to_assistant")
async def _notify_event(queue_item: ConversationEventQueueItem) -> None:
if stop_signal.is_set():
logger.warning(
"ignoring event due to stop signal; conversation_id: %s, event: %s, id: %s",
queue_item.event.conversation_id,
queue_item.event.event,
queue_item.event.id,
)
return
logger.debug(
"received event to notify; conversation_id: %s, event: %s, event_id: %s, audience: %s",
queue_item.event.conversation_id,
queue_item.event.event,
queue_item.event.id,
queue_item.event_audience,
)
if "user" in queue_item.event_audience:
enqueued_count = 0
async with conversation_sse_queues_lock:
for queue in conversation_sse_queues.get(queue_item.event.conversation_id, {}):
enqueued_count += 1
await queue.put(queue_item.event)
logger.debug(
"enqueued event for SSE; count: %d, conversation_id: %s, event: %s, event_id: %s",
enqueued_count,
queue_item.event.conversation_id,
queue_item.event.event,
queue_item.event.id,
)
if queue_item.event.event in [
ConversationEventType.message_created,
ConversationEventType.message_deleted,
ConversationEventType.conversation_updated,
ConversationEventType.participant_created,
ConversationEventType.participant_updated,
]:
task = asyncio.create_task(_notify_user_event(queue_item.event), name="notify_user_event")
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)
if "assistant" in queue_item.event_audience:
async with _controller_get_session() as session:
assistant_ids = (
await session.exec(
select(db.Assistant.assistant_id)
.join(
db.AssistantParticipant,
col(db.Assistant.assistant_id) == col(db.AssistantParticipant.assistant_id),
)
.join(db.AssistantServiceRegistration)
.where(col(db.AssistantServiceRegistration.assistant_service_online).is_(True))
.where(col(db.AssistantParticipant.active_participant).is_(True))
.where(db.AssistantParticipant.conversation_id == queue_item.event.conversation_id)
)
).all()
for assistant_id in assistant_ids:
if assistant_id not in assistant_event_queues:
queue = asyncio.Queue()
assistant_event_queues[assistant_id] = queue
task = asyncio.create_task(
_forward_events_to_assistant(assistant_id, queue),
name=f"forward_events_to_{assistant_id}",
)
background_tasks.add(task)
await assistant_event_queues[assistant_id].put(queue_item.event)
logger.debug(
"enqueued event for assistant; conversation_id: %s, event: %s, event_id: %s, assistant_id: %s",
queue_item.event.conversation_id,
queue_item.event.event,
queue_item.event.id,
assistant_id,
)
async def _notify_user_event(event: ConversationEvent) -> None:
listening_user_ids = set(user_sse_queues.keys())
async with _controller_get_session() as session:
active_user_participants = (
await session.exec(
select(db.UserParticipant.user_id).where(
col(db.UserParticipant.active_participant).is_(True),
db.UserParticipant.conversation_id == event.conversation_id,
col(db.UserParticipant.user_id).in_(listening_user_ids),
)
)
).all()
if not active_user_participants:
return
async with user_sse_queues_lock:
for user_id in active_user_participants:
for queue in user_sse_queues.get(user_id, {}):
await queue.put(event)
logger.debug(
"enqueued event for user SSE; user_id: %s, conversation_id: %s", user_id, event.conversation_id
)
assistant_client_pool = controller.AssistantServiceClientPool(api_key_store=api_key_store)
assistant_service_registration_controller = controller.AssistantServiceRegistrationController(
get_session=_controller_get_session,
notify_event=_notify_event,
api_key_store=api_key_store,
client_pool=assistant_client_pool,
)
app.add_middleware(
middleware.AuthMiddleware,
exclude_methods={"OPTIONS"},
exclude_paths=set(settings.service.anonymous_paths),
api_key_source=assistant_service_registration_controller.api_key_source,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
app.add_middleware(CorrelationIdMiddleware)
app.middleware("http")(log_request_middleware())
user_controller = controller.UserController(get_session=_controller_get_session)
assistant_controller = controller.AssistantController(
get_session=_controller_get_session,
notify_event=_notify_event,
client_pool=assistant_client_pool,
file_storage=files.Storage(settings.storage),
)
conversation_controller = controller.ConversationController(
get_session=_controller_get_session,
notify_event=_notify_event,
assistant_controller=assistant_controller,
)
conversation_share_controller = controller.ConversationShareController(
get_session=_controller_get_session,
notify_event=_notify_event,
)
file_controller = controller.FileController(
get_session=_controller_get_session,
notify_event=_notify_event,
file_storage=files.Storage(settings.storage),
)
@asynccontextmanager
async def _lifespan() -> AsyncIterator[None]:
async with db.create_engine(settings.db) as engine:
await db.bootstrap_db(engine, settings=settings.db)
app.state.db_engine = engine
background_tasks.add(
asyncio.create_task(
_update_assistant_service_online_status(), name="update_assistant_service_online_status"
),
)
try:
yield
finally:
stop_signal.set()
for task in background_tasks:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await asyncio.gather(*background_tasks, return_exceptions=True)
register_lifespan_handler(_lifespan)
async def _update_assistant_service_online_status() -> NoReturn:
while True:
try:
await asyncio.sleep(settings.service.assistant_service_online_check_interval_seconds)
await assistant_service_registration_controller.check_assistant_service_online_expired()
except Exception:
logger.exception("exception in _update_assistant_service_online_status")
@app.get("/")
async def root() -> Response:
return Response(status_code=status.HTTP_200_OK, content="")
@app.get("/users")
async def list_users(
user_ids: list[str] = Query(alias="id"),
) -> UserList:
return await user_controller.get_users(user_ids=user_ids)
@app.get("/users/me")
async def get_user_me(
user_principal: auth.DependsUserPrincipal,
) -> User:
return await user_controller.get_user_me(user_principal=user_principal)
@app.put("/users/me")
async def update_user_me(
user_principal: auth.DependsUserPrincipal,
update_user: UpdateUser,
) -> User:
return await user_controller.update_user(
user_principal=user_principal, user_id=user_principal.user_id, update_user=update_user
)
@app.post("/assistant-service-registrations")
async def create_assistant_service_registration(
user_principal: auth.DependsUserPrincipal,
new_assistant_service: NewAssistantServiceRegistration,
) -> AssistantServiceRegistration:
return await assistant_service_registration_controller.create_registration(
user_principal=user_principal, new_assistant_service=new_assistant_service
)
@app.put("/assistant-service-registrations/{assistant_service_id:path}")
async def update_assistant_service_registration_url(
principal: auth.DependsAssistantServicePrincipal,
assistant_service_id: str,
update_assistant_service: UpdateAssistantServiceRegistrationUrl,
background_tasks: BackgroundTasks,
) -> AssistantServiceRegistration:
registration, task_args = await assistant_service_registration_controller.update_assistant_service_url(
assistant_service_principal=principal,
assistant_service_id=assistant_service_id,
update_assistant_service_url=update_assistant_service,
)
if task_args:
background_tasks.add_task(*task_args)
return registration
@app.patch("/assistant-service-registrations/{assistant_service_id:path}")
async def update_assistant_service_registration(
principal: auth.DependsUserPrincipal,
assistant_service_id: str,
update_assistant_service: UpdateAssistantServiceRegistration,
) -> AssistantServiceRegistration:
return await assistant_service_registration_controller.update_registration(
user_principal=principal,
assistant_service_id=assistant_service_id,
update_assistant_service=update_assistant_service,
)
@app.post("/assistant-service-registrations/{assistant_service_id:path}/api-key")
async def reset_assistant_service_registration_api_key(
user_principal: auth.DependsUserPrincipal,
assistant_service_id: str,
) -> AssistantServiceRegistration:
return await assistant_service_registration_controller.reset_api_key(
user_principal=user_principal, assistant_service_id=assistant_service_id
)
@app.get("/assistant-service-registrations")
async def list_assistant_service_registrations(
user_principal: auth.DependsUserPrincipal,
user_ids: Annotated[list[str], Query(alias="user_id")] = [],
assistant_service_online: Annotated[bool | None, Query(alias="assistant_service_online")] = None,
) -> AssistantServiceRegistrationList:
user_id_set = set([user_principal.user_id if user_id == "me" else user_id for user_id in user_ids])
return await assistant_service_registration_controller.get_registrations(
user_ids=user_id_set, assistant_service_online=assistant_service_online
)
@app.get("/assistant-service-registrations/{assistant_service_id:path}")
async def get_assistant_service_registration(
user_principal: auth.DependsUserPrincipal, assistant_service_id: str
) -> AssistantServiceRegistration:
return await assistant_service_registration_controller.get_registration(
assistant_service_id=assistant_service_id
)
@app.delete(
"/assistant-service-registrations/{assistant_service_id:path}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_assistant_service(
user_principal: auth.DependsUserPrincipal,
assistant_service_id: str,
) -> None:
return await assistant_service_registration_controller.delete_registration(
user_principal=user_principal, assistant_service_id=assistant_service_id
)
@app.get("/assistant-services/{assistant_service_id:path}/info")
@app.get("/assistant-services/{assistant_service_id:path}")
async def get_assistant_service_info(
user_principal: auth.DependsUserPrincipal, assistant_service_id: str
) -> ServiceInfoModel:
return await assistant_service_registration_controller.get_service_info(
assistant_service_id=assistant_service_id
)
@app.get("/assistant-services")
async def list_assistant_service_infos(
principal: auth.DependsPrincipal,
user_ids: Annotated[list[str], Query(alias="user_id")] = [],
) -> AssistantServiceInfoList:
match principal:
case auth.UserPrincipal():
user_id_set = set([principal.user_id if user_id == "me" else user_id for user_id in user_ids])
case auth.AssistantServicePrincipal():
user_id_set = set(user_ids)
return await assistant_service_registration_controller.get_service_infos(user_ids=user_id_set)
@app.get("/assistants")
async def list_assistants(
user_principal: auth.DependsUserPrincipal, conversation_id: uuid.UUID | None = None
) -> AssistantList:
return await assistant_controller.get_assistants(user_principal=user_principal, conversation_id=conversation_id)
@app.get("/assistants/{assistant_id}")
async def get_assistant(user_principal: auth.DependsUserPrincipal, assistant_id: uuid.UUID) -> Assistant:
return await assistant_controller.get_assistant(user_principal=user_principal, assistant_id=assistant_id)
@app.post("/assistants", status_code=status.HTTP_201_CREATED)
async def create_assistant(
new_assistant: NewAssistant,
user_principal: auth.DependsUserPrincipal,
) -> Assistant:
return await assistant_controller.create_assistant(user_principal=user_principal, new_assistant=new_assistant)
@app.patch("/assistants/{assistant_id}")
async def update_assistant(
assistant_id: uuid.UUID,
update_assistant: UpdateAssistant,
user_principal: auth.DependsUserPrincipal,
) -> Assistant:
return await assistant_controller.update_assistant(
user_principal=user_principal, assistant_id=assistant_id, update_assistant=update_assistant
)
@app.get(
"/assistants/{assistant_id}/export", description="Export an assistant's configuration and conversation data."
)
async def export_assistant(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
) -> FileResponse:
result = await assistant_controller.export_assistant(user_principal=user_principal, assistant_id=assistant_id)
return FileResponse(
path=result.file_path,
media_type=result.content_type,
filename=result.filename,
background=starlette.background.BackgroundTask(result.cleanup),
)
@app.get(
"/conversations/export",
description="Export one or more conversations and the assistants that participate in them.",
)
async def export_conversations(
user_principal: auth.DependsUserPrincipal,
conversation_ids: list[uuid.UUID] = Query(alias="id"),
) -> FileResponse:
result = await assistant_controller.export_conversations(
user_principal=user_principal, conversation_ids=set(conversation_ids)
)
return FileResponse(
path=result.file_path,
media_type=result.content_type,
filename=result.filename,
background=starlette.background.BackgroundTask(result.cleanup),
)
@app.post("/conversations/import")
async def import_conversations(
from_export: Annotated[UploadFile, File(alias="from_export")],
user_principal: auth.DependsUserPrincipal,
) -> ConversationImportResult:
return await assistant_controller.import_conversations(
user_principal=user_principal, from_export=from_export.file
)
@app.get("/assistants/{assistant_id}/config")
async def get_assistant_config(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
) -> ConfigResponseModel:
return await assistant_controller.get_assistant_config(user_principal=user_principal, assistant_id=assistant_id)
@app.put("/assistants/{assistant_id}/config")
async def update_assistant_config(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
updated_config: ConfigPutRequestModel,
) -> ConfigResponseModel:
return await assistant_controller.update_assistant_config(
user_principal=user_principal,
assistant_id=assistant_id,
updated_config=updated_config,
)
@app.get("/assistants/{assistant_id}/conversations/{conversation_id}/states")
async def get_assistant_conversation_state_descriptions(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
conversation_id: uuid.UUID,
) -> StateDescriptionListResponseModel:
return await assistant_controller.get_assistant_conversation_state_descriptions(
user_principal=user_principal,
assistant_id=assistant_id,
conversation_id=conversation_id,
)
@app.get("/assistants/{assistant_id}/conversations/{conversation_id}/states/{state_id}")
async def get_assistant_conversation_state(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
conversation_id: uuid.UUID,
state_id: str,
) -> StateResponseModel:
return await assistant_controller.get_assistant_conversation_state(
user_principal=user_principal,
assistant_id=assistant_id,
conversation_id=conversation_id,
state_id=state_id,
)
@app.put("/assistants/{assistant_id}/conversations/{conversation_id}/states/{state_id}")
async def update_assistant_conversation_state(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
conversation_id: uuid.UUID,
state_id: str,
updated_state: StatePutRequestModel,
) -> StateResponseModel:
return await assistant_controller.update_assistant_conversation_state(
user_principal=user_principal,
assistant_id=assistant_id,
conversation_id=conversation_id,
state_id=state_id,
updated_state=updated_state,
)
@app.post("/assistants/{assistant_id}/states/events", status_code=status.HTTP_204_NO_CONTENT)
async def post_assistant_state_event(
assistant_id: uuid.UUID,
state_event: AssistantStateEvent,
assistant_principal: auth.DependsAssistantPrincipal,
conversation_id: Annotated[uuid.UUID | None, Query()] = None,
) -> None:
await assistant_controller.post_assistant_state_event(
assistant_id=assistant_id,
state_event=state_event,
assistant_principal=assistant_principal,
conversation_ids=[conversation_id] if conversation_id else [],
)
@app.delete(
"/assistants/{assistant_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_assistant(
user_principal: auth.DependsUserPrincipal,
assistant_id: uuid.UUID,
) -> None:
await assistant_controller.delete_assistant(
user_principal=user_principal,
assistant_id=assistant_id,
)
@app.get("/assistants/{assistant_id}/conversations")
async def get_assistant_conversations(
assistant_id: uuid.UUID,
user_principal: auth.DependsUserPrincipal,
latest_message_types: Annotated[list[MessageType], Query(alias="latest_message_type")] = [MessageType.chat],
) -> ConversationList:
return await conversation_controller.get_assistant_conversations(
user_principal=user_principal,
assistant_id=assistant_id,
latest_message_types=set(latest_message_types),
)
@app.get("/conversations/{conversation_id}/events")
async def conversation_server_sent_events(
conversation_id: uuid.UUID, request: Request, principal: auth.DependsActorPrincipal
) -> EventSourceResponse:
# ensure the principal has access to the conversation
await conversation_controller.get_conversation(
conversation_id=conversation_id,
principal=principal,
latest_message_types=set(),
)
principal_id_type = "assistant_id" if isinstance(principal, auth.AssistantPrincipal) else "user_id"
principal_id = principal.assistant_id if isinstance(principal, auth.AssistantPrincipal) else principal.user_id
logger.debug(
"client connected to sse; %s: %s, conversation_id: %s",
principal_id_type,
principal_id,
conversation_id,
)
event_queue = asyncio.Queue[ConversationEvent]()
async with conversation_sse_queues_lock:
queues = conversation_sse_queues[conversation_id]
queues.add(event_queue)
async def event_generator() -> AsyncIterator[ServerSentEvent]:
try:
while True:
if stop_signal.is_set():
logger.debug("sse stopping due to signal; conversation_id: %s", conversation_id)
break
try:
if await request.is_disconnected():
logger.debug("client disconnected from sse; conversation_id: %s", conversation_id)
break
except Exception:
logger.exception(
"error checking if client disconnected from sse; conversation_id: %s", conversation_id
)
break
try:
try:
async with asyncio.timeout(1):
conversation_event = await event_queue.get()
except asyncio.TimeoutError:
continue
server_sent_event = ServerSentEvent(
id=conversation_event.id,
event=conversation_event.event.value,
data=conversation_event.model_dump_json(include={"timestamp", "data"}),
retry=1000,
)
yield server_sent_event
logger.debug(
"sent event to sse client; %s: %s, conversation_id: %s, event: %s, id: %s, time since"
" event: %s",
principal_id_type,
principal_id,
conversation_id,
conversation_event.event,
conversation_event.id,
datetime.datetime.now(datetime.UTC) - conversation_event.timestamp,
)
except Exception:
logger.exception("error sending event to sse client; conversation_id: %s", conversation_id)
finally:
queues.discard(event_queue)
if len(queues) == 0:
async with conversation_sse_queues_lock:
if len(queues) == 0:
conversation_sse_queues.pop(conversation_id, None)
return EventSourceResponse(event_generator(), sep="\n")
@app.get("/events")
async def user_server_sent_events(
request: Request, user_principal: auth.DependsUserPrincipal
) -> EventSourceResponse:
logger.debug("client connected to user events sse; user_id: %s", user_principal.user_id)
event_queue = asyncio.Queue[ConversationEvent]()
async with user_sse_queues_lock:
queues = user_sse_queues[user_principal.user_id]
queues.add(event_queue)
async def event_generator() -> AsyncIterator[ServerSentEvent]:
try:
while True:
if stop_signal.is_set():
logger.debug("sse stopping due to signal; user_id: %s", user_principal.user_id)
break
try:
if await request.is_disconnected():
logger.debug("client disconnected from sse; user_id: %s", user_principal.user_id)
break
except Exception:
logger.exception(
"error checking if client disconnected from sse; user_id: %s", user_principal.user_id
)
break
try:
try:
async with asyncio.timeout(1):
conversation_event = await event_queue.get()
except asyncio.TimeoutError:
continue
server_sent_event = ServerSentEvent(
id=conversation_event.id,
event=conversation_event.event.value,
data=json.dumps({
**conversation_event.model_dump(mode="json", include={"timestamp", "data"}),
"conversation_id": str(conversation_event.conversation_id),
}),
retry=1000,
)
yield server_sent_event
logger.debug(
"sent event to user sse client; user_id: %s, event: %s",
user_principal.user_id,
server_sent_event.event,
)
except Exception:
logger.exception("error sending event to sse client; user_id: %s", user_principal.user_id)
finally:
queues.discard(event_queue)
if len(queues) == 0:
async with conversation_sse_queues_lock:
if len(queues) == 0:
user_sse_queues.pop(user_principal.user_id, None)
return EventSourceResponse(event_generator(), sep="\n")
@app.post("/conversations")
async def create_conversation(
new_conversation: NewConversation,
user_principal: auth.DependsUserPrincipal,
) -> Conversation:
return await conversation_controller.create_conversation(
user_principal=user_principal,
new_conversation=new_conversation,
)
@app.post("/conversations/{owner_id}")
async def create_conversation_with_owner(
assistant_principal: auth.DependsAssistantPrincipal,
new_conversation: NewConversation,
owner_id: str,
) -> Conversation:
return await conversation_controller.create_conversation_with_owner(
new_conversation=new_conversation,
principal=assistant_principal,
owner_id=owner_id,
)
@app.post("/conversations/{conversation_id}")
async def duplicate_conversation(
conversation_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
new_conversation: NewConversation,
) -> ConversationImportResult:
return await assistant_controller.duplicate_conversation(
principal=principal, conversation_id=conversation_id, new_conversation=new_conversation
)
@app.get("/conversations")
async def list_conversations(
principal: auth.DependsActorPrincipal,
include_inactive: bool = False,
latest_message_types: Annotated[list[MessageType], Query(alias="latest_message_type")] = [MessageType.chat],
) -> ConversationList:
return await conversation_controller.get_conversations(
principal=principal,
include_all_owned=include_inactive,
latest_message_types=set(latest_message_types),
)
@app.get("/conversations/{conversation_id}")
async def get_conversation(
conversation_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
latest_message_types: Annotated[list[MessageType], Query(alias="latest_message_type")] = [MessageType.chat],
) -> Conversation:
return await conversation_controller.get_conversation(
principal=principal,
conversation_id=conversation_id,
latest_message_types=set(latest_message_types),
)
@app.patch("/conversations/{conversation_id}")
async def update_conversation(
conversation_id: uuid.UUID,
update_conversation: UpdateConversation,
user_principal: auth.DependsActorPrincipal,
) -> Conversation:
return await conversation_controller.update_conversation(
user_principal=user_principal,
conversation_id=conversation_id,
update_conversation=update_conversation,
)
@app.get("/conversations/{conversation_id}/participants")
async def list_conversation_participants(
conversation_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
include_inactive: bool = False,
) -> ConversationParticipantList:
return await conversation_controller.get_conversation_participants(
principal=principal,
conversation_id=conversation_id,
include_inactive=include_inactive,
)
def _translate_participant_id_me(principal: auth.ActorPrincipal, participant_id: str) -> str:
if participant_id != "me":
return participant_id
match principal:
case auth.UserPrincipal():
return principal.user_id
case auth.AssistantPrincipal():
return str(principal.assistant_id)
@app.get("/conversations/{conversation_id}/participants/{participant_id}")
async def get_conversation_participant(
conversation_id: uuid.UUID,
participant_id: str,
principal: auth.DependsActorPrincipal,
) -> ConversationParticipant:
participant_id = _translate_participant_id_me(principal, participant_id)
return await conversation_controller.get_conversation_participant(
principal=principal,
conversation_id=conversation_id,
participant_id=participant_id,
)
@app.patch("/conversations/{conversation_id}/participants/{participant_id}")
@app.put("/conversations/{conversation_id}/participants/{participant_id}")
async def add_or_update_conversation_participant(
conversation_id: uuid.UUID,
participant_id: str,
update_participant: UpdateParticipant,
principal: auth.DependsActorPrincipal,
) -> ConversationParticipant:
participant_id = _translate_participant_id_me(principal, participant_id)
return await conversation_controller.add_or_update_conversation_participant(
participant_id=participant_id,
update_participant=update_participant,
conversation_id=conversation_id,
principal=principal,
)
@app.get("/conversations/{conversation_id}/messages")
async def list_conversation_messages(
conversation_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
participant_roles: Annotated[list[ParticipantRole] | None, Query(alias="participant_role")] = None,
participant_ids: Annotated[list[str] | None, Query(alias="participant_id")] = None,
message_types: Annotated[list[MessageType] | None, Query(alias="message_type")] = None,
before: Annotated[uuid.UUID | None, Query()] = None,
after: Annotated[uuid.UUID | None, Query()] = None,
limit: Annotated[int, Query(lte=500)] = 100,
) -> ConversationMessageList:
return await conversation_controller.get_messages(
conversation_id=conversation_id,
principal=principal,
participant_ids=participant_ids,
participant_roles=participant_roles,
message_types=message_types,
before=before,
after=after,
limit=limit,
)
@app.post("/conversations/{conversation_id}/messages")
async def create_conversation_message(
conversation_id: uuid.UUID,
new_message: NewConversationMessage,
principal: auth.DependsActorPrincipal,
background_tasks: BackgroundTasks,
) -> ConversationMessage:
response, task_args = await conversation_controller.create_conversation_message(
conversation_id=conversation_id,
new_message=new_message,
principal=principal,
)
if task_args:
background_tasks.add_task(*task_args)
return response
@app.get(
"/conversations/{conversation_id}/messages/{message_id}",
)
async def get_message(
conversation_id: uuid.UUID,
message_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
) -> ConversationMessage:
return await conversation_controller.get_message(
conversation_id=conversation_id,
message_id=message_id,
principal=principal,
)
@app.get(
"/conversations/{conversation_id}/messages/{message_id}/debug_data",
)
async def get_message_debug_data(
conversation_id: uuid.UUID,
message_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
) -> ConversationMessageDebug:
return await conversation_controller.get_message_debug(
conversation_id=conversation_id,
message_id=message_id,
principal=principal,
)
@app.delete(
"/conversations/{conversation_id}/messages/{message_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_message(
conversation_id: uuid.UUID,
message_id: uuid.UUID,
user_principal: auth.DependsUserPrincipal,
) -> None:
await conversation_controller.delete_message(
conversation_id=conversation_id,
message_id=message_id,
user_principal=user_principal,
)
@app.put("/conversations/{conversation_id}/files")
async def upload_files(
conversation_id: uuid.UUID,
upload_files: Annotated[list[UploadFile], File(alias="files")],
principal: auth.DependsActorPrincipal,
file_metadata_raw: str = Form(alias="metadata", default="{}"),
) -> FileList:
try:
file_metadata: dict[str, dict[str, Any]] = json.loads(file_metadata_raw)
except json.JSONDecodeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
if not isinstance(file_metadata, dict):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="metadata must be a JSON object as a string"
)
return await file_controller.upload_files(
conversation_id=conversation_id,
upload_files=upload_files,
principal=principal,
file_metadata=file_metadata,
)
@app.get("/conversations/{conversation_id}/files")
async def list_files(
conversation_id: uuid.UUID,
principal: auth.DependsActorPrincipal,
prefix: str | None = None,
) -> FileList:
return await file_controller.list_files(conversation_id=conversation_id, principal=principal, prefix=prefix)
@app.get("/conversations/{conversation_id}/files/{filename:path}/versions")
async def file_versions(
conversation_id: uuid.UUID,
filename: str,
principal: auth.DependsActorPrincipal,
version: int | None = None,
) -> FileVersions:
return await file_controller.file_versions(
conversation_id=conversation_id, filename=filename, principal=principal, version=version
)
@app.get("/conversations/{conversation_id}/files/{filename:path}")
async def download_file(
conversation_id: uuid.UUID,
filename: str,
principal: auth.DependsActorPrincipal,
version: int | None = None,
) -> StreamingResponse:
result = await file_controller.download_file(
conversation_id=conversation_id,
filename=filename,
principal=principal,
version=version,
)
return StreamingResponse(
result.stream,
media_type=result.content_type,
headers={"Content-Disposition": f'attachment; filename="{urllib.parse.quote(result.filename)}"'},
)
@app.patch("/conversations/{conversation_id}/files/{filename:path}")
async def update_file(
conversation_id: uuid.UUID,
filename: str,
principal: auth.DependsActorPrincipal,
update_file: UpdateFile,
) -> FileVersions:
return await file_controller.update_file_metadata(
conversation_id=conversation_id,
filename=filename,
principal=principal,
metadata=update_file.metadata,
)
@app.delete("/conversations/{conversation_id}/files/{filename:path}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_file(
conversation_id: uuid.UUID,
filename: str,
principal: auth.DependsActorPrincipal,
) -> None:
await file_controller.delete_file(
conversation_id=conversation_id,
filename=filename,
principal=principal,
)
@app.post("/conversation-shares")
async def create_conversation_share(
user_principal: auth.DependsUserPrincipal,
new_conversation_share: NewConversationShare,
) -> ConversationShare:
return await conversation_share_controller.create_conversation_share(
user_principal=user_principal,
new_conversation_share=new_conversation_share,
)
# create_conversation_share_with_owner
@app.post("/conversation-shares/{owner_id}")
async def create_conversation_share_with_owner(
new_conversation_share: NewConversationShare,
owner_id: str,
) -> ConversationShare:
return await conversation_share_controller.create_conversation_share_with_owner(
new_conversation_share=new_conversation_share,
owner_id=owner_id,
)
@app.get("/conversation-shares")
async def list_conversation_shares(
user_principal: auth.DependsUserPrincipal,
include_unredeemable: bool = False,
conversation_id: uuid.UUID | None = None,
) -> ConversationShareList:
return await conversation_share_controller.get_conversation_shares(
user_principal=user_principal,
conversation_id=conversation_id,
include_unredeemable=include_unredeemable,
)
@app.get("/conversation-shares/{conversation_share_id}")
async def get_conversation_share(
user_principal: auth.DependsUserPrincipal,
conversation_share_id: uuid.UUID,
) -> ConversationShare:
return await conversation_share_controller.get_conversation_share(
user_principal=user_principal,
conversation_share_id=conversation_share_id,
)
@app.delete("/conversation-shares/{conversation_share_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation_share(
user_principal: auth.DependsUserPrincipal,
conversation_share_id: uuid.UUID,
) -> None:
await conversation_share_controller.delete_conversation_share(
user_principal=user_principal,
conversation_share_id=conversation_share_id,
)
@app.post("/conversation-shares/{conversation_share_id}/redemptions")
async def redeem_conversation_share(
user_principal: auth.DependsUserPrincipal,
conversation_share_id: uuid.UUID,
) -> ConversationShareRedemption:
return await conversation_share_controller.redeem_conversation_share(
user_principal=user_principal,
conversation_share_id=conversation_share_id,
)
@app.get("/conversation-shares/{conversation_share_id}/redemptions")
async def list_conversation_share_redemptions(
user_principal: auth.DependsUserPrincipal,
conversation_share_id: uuid.UUID,
) -> ConversationShareRedemptionList:
return await conversation_share_controller.get_redemptions_for_share(
user_principal=user_principal,
conversation_share_id=conversation_share_id,
)
@app.get("/conversation-share-redemptions")
async def list_conversation_share_redemptions_for_user(
user_principal: auth.DependsUserPrincipal,
) -> ConversationShareRedemptionList:
return await conversation_share_controller.get_redemptions_for_user(
user_principal=user_principal,
)
@app.get("/azure-speech/token")
async def get_azure_speech_token() -> dict[str, str]:
return azure_speech.get_token()
```
--------------------------------------------------------------------------------
/workbench-service/semantic_workbench_service/controller/conversation.py:
--------------------------------------------------------------------------------
```python
import datetime
import logging
import uuid
from typing import (
Annotated,
AsyncContextManager,
Awaitable,
Callable,
Iterable,
Literal,
Sequence,
)
import deepmerge
import openai_client
from openai.types.chat import ChatCompletionMessageParam
from pydantic import BaseModel, Field, HttpUrl
from semantic_workbench_api_model.assistant_service_client import AssistantError
from semantic_workbench_api_model.workbench_model import (
Conversation,
ConversationEvent,
ConversationEventType,
ConversationList,
ConversationMessage,
ConversationMessageDebug,
ConversationMessageList,
ConversationParticipant,
ConversationParticipantList,
MessageType,
NewConversation,
NewConversationMessage,
ParticipantRole,
UpdateConversation,
UpdateParticipant,
)
from sqlalchemy.orm.attributes import flag_modified
from sqlmodel import and_, col, or_, select
from sqlmodel.ext.asyncio.session import AsyncSession
from .. import auth, db, query, settings
from ..event import ConversationEventQueueItem
from . import assistant, convert, exceptions
from . import participant as participant_
from . import user as user_
logger = logging.getLogger(__name__)
class ConversationTitleResponse(BaseModel):
"""Model for responses from LLM for automatic conversation re-titling."""
title: Annotated[
str,
Field(
description="The updated title of the conversation. If the subject matter of the conversation has changed significantly from the current title, suggest a short, but descriptive title for the conversation. Ideally 4 words or less in length. Leave it blank to keep the current title.",
),
]
META_DATA_KEY_USER_SET_TITLE = "__user_set_title"
META_DATA_KEY_AUTO_TITLE_COUNT = "__auto_title_count"
AUTO_TITLE_COUNT_LIMIT = 3
"""
The maximum number of times a conversation can be automatically retitled.
"""
class ConversationController:
def __init__(
self,
get_session: Callable[[], AsyncContextManager[AsyncSession]],
notify_event: Callable[[ConversationEventQueueItem], Awaitable],
assistant_controller: assistant.AssistantController,
) -> None:
self._get_session = get_session
self._notify_event = notify_event
self._assistant_controller = assistant_controller
async def create_conversation(
self,
new_conversation: NewConversation,
user_principal: auth.UserPrincipal,
) -> Conversation:
async with self._get_session() as session:
await user_.add_or_update_user_from(session=session, user_principal=user_principal)
conversation = db.Conversation(
owner_id=user_principal.user_id,
title=new_conversation.title or NewConversation().title,
meta_data=new_conversation.metadata,
imported_from_conversation_id=None,
)
if new_conversation.title and new_conversation.title != NewConversation().title:
conversation.meta_data = {
**conversation.meta_data,
META_DATA_KEY_USER_SET_TITLE: True,
}
session.add(conversation)
session.add(
db.UserParticipant(
conversation_id=conversation.conversation_id,
user_id=user_principal.user_id,
conversation_permission="read_write",
)
)
await session.commit()
await session.refresh(conversation)
return await self.get_conversation(
conversation_id=conversation.conversation_id,
principal=user_principal,
latest_message_types=set(),
)
async def create_conversation_with_owner(
self,
new_conversation: NewConversation,
owner_id: str,
principal: auth.AssistantPrincipal,
) -> Conversation:
async with self._get_session() as session:
conversation = db.Conversation(
owner_id=owner_id,
title=new_conversation.title or NewConversation().title,
meta_data=new_conversation.metadata,
imported_from_conversation_id=None,
)
if new_conversation.title and new_conversation.title != NewConversation().title:
conversation.meta_data = {
**conversation.meta_data,
META_DATA_KEY_USER_SET_TITLE: True,
}
session.add(conversation)
# session.add(
# db.UserParticipant(
# conversation_id=conversation.conversation_id,
# user_id=owner_id,
# conversation_permission="read_write",
# )
# )
session.add(
db.AssistantParticipant(
conversation_id=conversation.conversation_id,
assistant_id=principal.assistant_id,
)
)
await session.commit()
await session.refresh(conversation)
assistant = (
await session.exec(select(db.Assistant).where(db.Assistant.assistant_id == principal.assistant_id))
).one_or_none()
if assistant is None:
raise exceptions.NotFoundError()
await self._assistant_controller.connect_assistant_to_conversation(
assistant=assistant,
conversation=conversation,
from_export=None,
)
return await self.get_conversation(
conversation_id=conversation.conversation_id,
principal=principal,
latest_message_types=set(),
)
async def _projections_with_participants(
self,
session: AsyncSession,
conversation_projections: Sequence[tuple[db.Conversation, db.ConversationMessage | None, bool, str]],
) -> Iterable[
tuple[
db.Conversation,
Iterable[db.UserParticipant],
Iterable[db.AssistantParticipant],
dict[uuid.UUID, db.Assistant],
db.ConversationMessage | None,
bool,
str,
]
]:
user_participants = (
await session.exec(
select(db.UserParticipant).where(
col(db.UserParticipant.conversation_id).in_([
c[0].conversation_id for c in conversation_projections
])
)
)
).all()
assistant_participants = (
await session.exec(
select(db.AssistantParticipant).where(
col(db.AssistantParticipant.conversation_id).in_([
c[0].conversation_id for c in conversation_projections
])
)
)
).all()
assistants = (
await session.exec(
select(db.Assistant).where(
col(db.Assistant.assistant_id).in_([p.assistant_id for p in assistant_participants])
)
)
).all()
assistants_map = {assistant.assistant_id: assistant for assistant in assistants}
def merge() -> Iterable[
tuple[
db.Conversation,
Iterable[db.UserParticipant],
Iterable[db.AssistantParticipant],
dict[uuid.UUID, db.Assistant],
db.ConversationMessage | None,
bool,
str,
]
]:
for (
conversation,
latest_message,
latest_message_has_debug,
permission,
) in conversation_projections:
conversation_id = conversation.conversation_id
conversation_user_participants = (
up for up in user_participants if up.conversation_id == conversation_id
)
conversation_assistant_participants = (
ap for ap in assistant_participants if ap.conversation_id == conversation_id
)
yield (
conversation,
conversation_user_participants,
conversation_assistant_participants,
assistants_map,
latest_message,
latest_message_has_debug,
permission,
)
return merge()
async def get_conversations(
self,
principal: auth.ActorPrincipal,
latest_message_types: set[MessageType],
include_all_owned: bool = False,
) -> ConversationList:
async with self._get_session() as session:
include_all_owned = include_all_owned and isinstance(principal, auth.UserPrincipal)
conversation_projections = (
await session.exec(
query.select_conversation_projections_for(
principal=principal,
include_all_owned=include_all_owned,
include_observer=True,
latest_message_types=latest_message_types,
).order_by(col(db.Conversation.created_datetime).desc())
)
).all()
projections_with_participants = await self._projections_with_participants(
session=session, conversation_projections=conversation_projections
)
return convert.conversation_list_from_db(models=projections_with_participants)
async def get_assistant_conversations(
self,
user_principal: auth.UserPrincipal,
assistant_id: uuid.UUID,
latest_message_types: set[MessageType],
) -> ConversationList:
async with self._get_session() as session:
assistant = (
await session.exec(
query.select_assistants_for(user_principal=user_principal).where(
db.Assistant.assistant_id == assistant_id
)
)
).one_or_none()
if assistant is None:
raise exceptions.NotFoundError()
conversation_projections = (
await session.exec(
query.select_conversation_projections_for(
principal=auth.AssistantPrincipal(
assistant_service_id=assistant.assistant_service_id,
assistant_id=assistant_id,
),
latest_message_types=latest_message_types,
)
)
).all()
projections_with_participants = await self._projections_with_participants(
session=session, conversation_projections=conversation_projections
)
return convert.conversation_list_from_db(models=projections_with_participants)
async def get_conversation(
self,
conversation_id: uuid.UUID,
principal: auth.ActorPrincipal,
latest_message_types: set[MessageType],
) -> Conversation:
async with self._get_session() as session:
include_all_owned = isinstance(principal, auth.UserPrincipal)
conversation_projection = (
await session.exec(
query.select_conversation_projections_for(
principal=principal,
include_all_owned=include_all_owned,
include_observer=True,
latest_message_types=latest_message_types,
).where(db.Conversation.conversation_id == conversation_id)
)
).one_or_none()
if conversation_projection is None:
raise exceptions.NotFoundError()
projections_with_participants = await self._projections_with_participants(
session=session,
conversation_projections=[conversation_projection],
)
(
conversation,
user_participants,
assistant_participants,
assistants,
latest_message,
latest_message_has_debug,
permission,
) = next(iter(projections_with_participants))
return convert.conversation_from_db(
model=conversation,
latest_message=latest_message,
latest_message_has_debug=latest_message_has_debug,
permission=permission,
user_participants=user_participants,
assistant_participants=assistant_participants,
assistants=assistants,
)
async def update_conversation(
self,
conversation_id: uuid.UUID,
update_conversation: UpdateConversation,
user_principal: auth.ActorPrincipal,
) -> Conversation:
async with self._get_session() as session:
conversation = (
await session.exec(
query.select_conversations_for(
principal=user_principal,
include_all_owned=True,
)
.where(
db.Conversation.conversation_id == conversation_id,
)
.with_for_update()
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
for key, value in update_conversation.model_dump(exclude_unset=True).items():
match key:
case "metadata":
system_entries = {k: v for k, v in conversation.meta_data.items() if k.startswith("__")}
conversation.meta_data = {
**conversation.meta_data,
**value,
**system_entries,
}
case "title":
if value == conversation.title:
continue
conversation.title = value
conversation.meta_data = {
**conversation.meta_data,
META_DATA_KEY_USER_SET_TITLE: True,
}
case _:
setattr(conversation, key, value)
session.add(conversation)
await session.commit()
await session.refresh(conversation)
conversation_model = await self.get_conversation(
conversation_id=conversation.conversation_id,
principal=user_principal,
latest_message_types=set(),
)
await self._notify_event(
ConversationEventQueueItem(
event=ConversationEvent(
conversation_id=conversation.conversation_id,
event=ConversationEventType.conversation_updated,
data={
"conversation": conversation_model.model_dump(),
},
)
)
)
return conversation_model
async def get_conversation_participants(
self,
conversation_id: uuid.UUID,
principal: auth.ActorPrincipal,
include_inactive: bool = False,
) -> ConversationParticipantList:
async with self._get_session() as session:
conversation = (
await session.exec(
query.select_conversations_for(
principal=principal,
include_all_owned=True,
include_observer=True,
).where(db.Conversation.conversation_id == conversation_id)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
return await participant_.get_conversation_participants(
session=session,
conversation_id=conversation.conversation_id,
include_inactive=include_inactive,
)
async def get_conversation_participant(
self,
conversation_id: uuid.UUID,
participant_id: str,
principal: auth.ActorPrincipal,
) -> ConversationParticipant:
async with self._get_session() as session:
conversation = (
await session.exec(
query.select_conversations_for(
principal=principal,
include_all_owned=True,
include_observer=True,
).where(db.Conversation.conversation_id == conversation_id)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
possible_user_participant = (
await session.exec(
select(db.UserParticipant)
.where(db.UserParticipant.conversation_id == conversation.conversation_id)
.where(db.UserParticipant.user_id == participant_id)
)
).one_or_none()
if possible_user_participant is not None:
return convert.conversation_participant_from_db_user(possible_user_participant)
possible_assistant_participant = (
await session.exec(
select(db.AssistantParticipant)
.where(db.AssistantParticipant.conversation_id == conversation.conversation_id)
.where(db.AssistantParticipant.assistant_id == participant_id)
)
).one_or_none()
if possible_assistant_participant is not None:
assistant = (
await session.exec(
select(db.Assistant).where(
db.Assistant.assistant_id == possible_assistant_participant.assistant_id
)
)
).one_or_none()
return convert.conversation_participant_from_db_assistant(
possible_assistant_participant, assistant=assistant
)
raise exceptions.NotFoundError()
async def add_or_update_conversation_participant(
self,
conversation_id: uuid.UUID,
participant_id: str,
update_participant: UpdateParticipant,
principal: auth.ActorPrincipal,
) -> ConversationParticipant:
if update_participant.active_participant is not None and not update_participant.active_participant:
update_participant.status = None
async with self._get_session() as session:
async def update_user_participant(
conversation: db.Conversation, user: db.User
) -> tuple[
ConversationParticipant,
Literal[ConversationEventType.participant_updated,] | None,
]:
event_type: ConversationEventType | None = None
participant = (
await session.exec(
select(db.UserParticipant)
.join(db.Conversation)
.where(db.UserParticipant.conversation_id == conversation.conversation_id)
.where(db.UserParticipant.user_id == user.user_id)
.where(
or_(
col(db.UserParticipant.active_participant).is_(True),
db.Conversation.owner_id == user.user_id,
)
)
.with_for_update()
)
).one_or_none()
if participant is None:
raise exceptions.NotFoundError()
if update_participant.active_participant is not None:
event_type = ConversationEventType.participant_updated
participant.active_participant = update_participant.active_participant
if update_participant.status != participant.status:
event_type = event_type or ConversationEventType.participant_updated
participant.status = update_participant.status
participant.status_updated_datetime = datetime.datetime.now(datetime.UTC)
if update_participant.metadata is not None:
event_type = event_type or ConversationEventType.participant_updated
participant.meta_data = deepmerge.always_merger.merge(
{**participant.meta_data}, update_participant.metadata
)
if event_type is not None:
session.add(participant)
await session.commit()
await session.refresh(participant)
return convert.conversation_participant_from_db_user(participant), event_type
async def update_assistant_participant(
conversation: db.Conversation,
assistant: db.Assistant,
) -> tuple[
ConversationParticipant,
Literal[
ConversationEventType.participant_created,
ConversationEventType.participant_updated,
]
| None,
]:
new_participant = await db.insert_if_not_exists(
session,
db.AssistantParticipant(
conversation_id=conversation.conversation_id,
assistant_id=assistant.assistant_id,
status=update_participant.status,
name=assistant.name,
image=assistant.image,
),
)
event_type = ConversationEventType.participant_created if new_participant else None
participant = (
await session.exec(
select(db.AssistantParticipant)
.where(db.AssistantParticipant.conversation_id == conversation.conversation_id)
.where(db.AssistantParticipant.assistant_id == assistant.assistant_id)
.with_for_update()
)
).one()
original_participant = participant.model_copy(deep=True)
active_participant_changed = new_participant
if update_participant.active_participant is not None:
event_type = event_type or ConversationEventType.participant_updated
active_participant_changed = active_participant_changed or (
participant.active_participant != update_participant.active_participant
)
participant.active_participant = update_participant.active_participant
if participant.status != update_participant.status:
event_type = event_type or ConversationEventType.participant_updated
participant.status = update_participant.status
participant.status_updated_datetime = datetime.datetime.now(datetime.UTC)
if update_participant.metadata is not None:
event_type = event_type or ConversationEventType.participant_updated
participant.meta_data = {
**deepmerge.always_merger.merge(participant.meta_data.copy(), update_participant.metadata)
}
flag_modified(participant, "meta_data")
if event_type is not None:
session.add(participant)
await session.commit()
await session.refresh(participant)
if active_participant_changed and participant.active_participant:
try:
await self._assistant_controller.connect_assistant_to_conversation(
assistant=assistant,
conversation=conversation,
from_export=None,
)
except AssistantError:
logger.error(
f"failed to connect assistant {assistant.assistant_id} to conversation"
f" {conversation.conversation_id}",
exc_info=True,
)
session.add(original_participant)
await session.commit()
raise
if active_participant_changed and not participant.active_participant:
try:
await self._assistant_controller.disconnect_assistant_from_conversation(
assistant=assistant,
conversation_id=conversation.conversation_id,
)
except AssistantError:
logger.error(
f"failed to disconnect assistant {assistant.assistant_id} from conversation"
f" {conversation.conversation_id}",
exc_info=True,
)
return (
convert.conversation_participant_from_db_assistant(
participant,
assistant=assistant,
),
event_type,
)
match principal:
case auth.UserPrincipal():
await user_.add_or_update_user_from(user_principal=principal, session=session)
# users can update participants in any conversation they own or are participants of
conversation = (
await session.exec(
query.select_conversations_for(
principal=principal,
include_all_owned=True,
include_observer=True,
).where(db.Conversation.conversation_id == conversation_id)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
assistant_id: uuid.UUID | None = None
try:
assistant_id = uuid.UUID(participant_id)
participant_role = "assistant"
except ValueError:
participant_role = "user"
match participant_role:
case "user":
# users can only update their own participant
if participant_id != principal.user_id:
raise exceptions.ForbiddenError()
user = (
await session.exec(select(db.User).where(db.User.user_id == participant_id))
).one_or_none()
if user is None:
raise exceptions.NotFoundError()
participant, event_type = await update_user_participant(conversation, user)
case "assistant":
# users can add any of their assistants to conversation
assistant = (
await session.exec(
query.select_assistants_for(user_principal=principal).where(
db.Assistant.assistant_id == assistant_id
)
)
).one_or_none()
if assistant is None:
raise exceptions.NotFoundError()
(
participant,
event_type,
) = await update_assistant_participant(conversation, assistant)
case auth.AssistantServicePrincipal():
# assistants can update participants in conversations they are already participants of
conversation = (
await session.exec(
query.select_conversations_for(principal=principal).where(
db.Conversation.conversation_id == conversation_id
)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
# assistants can only update their own status
if participant_id != str(principal.assistant_id):
raise exceptions.ForbiddenError()
assistant = (
await session.exec(
select(db.Assistant).where(db.Assistant.assistant_id == principal.assistant_id)
)
).one_or_none()
if assistant is None:
raise exceptions.NotFoundError()
if assistant.assistant_service_id != principal.assistant_service_id:
raise exceptions.ForbiddenError()
participant, event_type = await update_assistant_participant(conversation, assistant)
if event_type is not None:
participants = await participant_.get_conversation_participants(
session=session,
conversation_id=conversation.conversation_id,
include_inactive=True,
)
await self._notify_event(
ConversationEventQueueItem(
event=participant_.participant_event(
event_type=event_type,
conversation_id=conversation.conversation_id,
participant=participant,
participants=participants,
),
)
)
return participant
async def create_conversation_message(
self,
principal: auth.ActorPrincipal,
conversation_id: uuid.UUID,
new_message: NewConversationMessage,
) -> tuple[ConversationMessage, Iterable]:
async with self._get_session() as session:
conversation = (
await session.exec(
query.select_conversations_for(principal=principal).where(
db.Conversation.conversation_id == conversation_id
)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
if (
new_message.id is not None
and (
await session.exec(
query.select(db.ConversationMessage)
.where(db.ConversationMessage.conversation_id == conversation_id)
.where(db.ConversationMessage.message_id == new_message.id)
)
).one_or_none()
is not None
):
raise exceptions.ConflictError(f"message with id {new_message.id} already exists")
match principal:
case auth.UserPrincipal():
role = "user"
participant_id = principal.user_id
case auth.AssistantServicePrincipal():
# allow assistants to send messages as users, if provided
if new_message.sender is not None and new_message.sender.participant_role == "user":
role = "user"
participant_id = new_message.sender.participant_id
else:
role = "assistant"
participant_id = str(principal.assistant_id)
# pop "debug" from metadata, if it exists, and merge with the debug field
message_debug = (new_message.metadata or {}).pop("debug", None)
# ensure that message_debug is a dictionary, in cases like {"debug": "some message"}, or {"debug": [1,2]}
if message_debug and not isinstance(message_debug, dict):
message_debug = {"debug": message_debug}
message_debug = deepmerge.always_merger.merge(message_debug or {}, new_message.debug_data or {})
message = db.ConversationMessage(
conversation_id=conversation.conversation_id,
sender_participant_role=role,
sender_participant_id=participant_id,
message_type=new_message.message_type.value,
content=new_message.content,
content_type=new_message.content_type,
filenames=new_message.filenames or [],
meta_data=new_message.metadata or {},
)
if new_message.id is not None:
message.message_id = new_message.id
session.add(message)
if message_debug:
debug = db.ConversationMessageDebug(
message_id=message.message_id,
data=message_debug,
)
session.add(debug)
await session.commit()
await session.refresh(message)
background_task: Iterable = ()
if self._conversation_candidate_for_retitling(
conversation=conversation
) and self._message_candidate_for_retitling(message=message):
background_task = (
self._retitle_conversation,
principal,
conversation_id,
message.sequence,
)
message_response = convert.conversation_message_from_db(message, has_debug=bool(message_debug))
await self._notify_event(
ConversationEventQueueItem(
event=ConversationEvent(
conversation_id=conversation_id,
event=ConversationEventType.message_created,
data={
"message": message_response.model_dump(),
},
),
)
)
return message_response, background_task
def _message_candidate_for_retitling(self, message: db.ConversationMessage) -> bool:
"""Check if the message is a candidate for retitling the conversation."""
if message.sender_participant_role != ParticipantRole.user.value:
return False
if message.message_type != MessageType.chat.value:
return False
return True
def _conversation_candidate_for_retitling(self, conversation: db.Conversation) -> bool:
"""Check if the conversation is a candidate for retitling."""
if conversation.meta_data.get(META_DATA_KEY_USER_SET_TITLE, False):
return False
if conversation.meta_data.get(META_DATA_KEY_AUTO_TITLE_COUNT, 0) >= AUTO_TITLE_COUNT_LIMIT:
return False
return True
async def _retitle_conversation(
self,
principal: auth.ActorPrincipal,
conversation_id: uuid.UUID,
latest_message_sequence: int,
) -> None:
"""Retitle the conversation based on the most recent messages."""
if not settings.service.azure_openai_endpoint:
logger.warning(
"Azure OpenAI endpoint is not configured, skipping retitling conversation %s",
conversation_id,
)
return
if not settings.service.azure_openai_deployment:
logger.warning(
"Azure OpenAI deployment is not configured, skipping retitling conversation %s",
conversation_id,
)
return
async with self._get_session() as session:
# Retrieve the most recent messages
messages = list(
(
await session.exec(
select(db.ConversationMessage)
.where(
db.ConversationMessage.conversation_id == conversation_id,
db.ConversationMessage.sequence <= latest_message_sequence,
db.ConversationMessage.message_type == MessageType.chat.value,
)
.order_by(col(db.ConversationMessage.sequence).desc())
.limit(10)
)
).all()
)
if not messages:
return
messages.reverse()
completion_messages: list[ChatCompletionMessageParam] = []
for message in messages:
match message.sender_participant_role:
case ParticipantRole.user.value:
completion_messages.append({
"role": "user",
"content": message.content,
})
case _:
completion_messages.append({
"role": "assistant",
"content": message.content,
})
# Call the LLM to get a new title
try:
async with openai_client.create_client(
openai_client.AzureOpenAIServiceConfig(
auth_config=openai_client.AzureOpenAIAzureIdentityAuthConfig(),
azure_openai_deployment=settings.service.azure_openai_deployment,
azure_openai_endpoint=HttpUrl(settings.service.azure_openai_endpoint),
),
) as client:
response = await client.beta.chat.completions.parse(
messages=[
*completion_messages,
{
"role": "developer",
"content": ("The current conversation title is: {conversation.title}"),
},
],
model=settings.service.azure_openai_model,
# the model's description also contains instructions
response_format=ConversationTitleResponse,
)
if not response.choices:
raise RuntimeError("No choices in azure openai response")
result = response.choices[0].message.parsed
if result is None:
raise RuntimeError("No parsed result in azure openai response")
except Exception:
logger.exception("Failed to retitle conversation %s", conversation_id)
return
# Update the conversation title if it has not already been changed from the default
async with self._get_session() as session:
conversation = (
await session.exec(
select(db.Conversation).where(db.Conversation.conversation_id == conversation_id).with_for_update()
)
).one_or_none()
if conversation is None:
return
if not self._conversation_candidate_for_retitling(conversation):
return
if result.title.strip():
conversation.title = result.title.strip()
conversation.meta_data = {
**conversation.meta_data,
META_DATA_KEY_AUTO_TITLE_COUNT: conversation.meta_data.get(META_DATA_KEY_AUTO_TITLE_COUNT, 0) + 1,
}
session.add(conversation)
await session.commit()
await session.refresh(conversation)
conversation_model = await self.get_conversation(
conversation_id=conversation.conversation_id,
principal=principal,
latest_message_types=set(),
)
await self._notify_event(
ConversationEventQueueItem(
event=ConversationEvent(
conversation_id=conversation.conversation_id,
event=ConversationEventType.conversation_updated,
data={
"conversation": conversation_model.model_dump(),
},
)
)
)
async def get_message(
self,
principal: auth.ActorPrincipal,
conversation_id: uuid.UUID,
message_id: uuid.UUID,
) -> ConversationMessage:
async with self._get_session() as session:
projection = (
await session.exec(
query.select_conversation_message_projections_for(principal=principal)
.where(db.ConversationMessage.conversation_id == conversation_id)
.where(db.ConversationMessage.message_id == message_id)
)
).one_or_none()
if projection is None:
raise exceptions.NotFoundError()
message, has_debug = projection
return convert.conversation_message_from_db(message, has_debug=has_debug)
async def get_message_debug(
self,
principal: auth.ActorPrincipal,
conversation_id: uuid.UUID,
message_id: uuid.UUID,
) -> ConversationMessageDebug:
async with self._get_session() as session:
message_debug = (
await session.exec(
query.select_conversation_message_debugs_for(principal=principal).where(
db.Conversation.conversation_id == conversation_id,
db.ConversationMessageDebug.message_id == message_id,
)
)
).one_or_none()
if message_debug is None:
raise exceptions.NotFoundError()
return convert.conversation_message_debug_from_db(message_debug)
async def get_messages(
self,
principal: auth.ActorPrincipal,
conversation_id: uuid.UUID,
participant_roles: list[ParticipantRole] | None = None,
participant_ids: list[str] | None = None,
message_types: list[MessageType] | None = None,
before: uuid.UUID | None = None,
after: uuid.UUID | None = None,
limit: int = 100,
) -> ConversationMessageList:
async with self._get_session() as session:
conversation = (
await session.exec(
query.select_conversations_for(principal=principal, include_observer=True).where(
db.Conversation.conversation_id == conversation_id
)
)
).one_or_none()
if conversation is None:
raise exceptions.NotFoundError()
select_query = query.select_conversation_message_projections_for(principal=principal).where(
db.ConversationMessage.conversation_id == conversation_id
)
if participant_roles is not None:
select_query = select_query.where(
col(db.ConversationMessage.sender_participant_role).in_([r.value for r in participant_roles])
)
if participant_ids is not None:
select_query = select_query.where(
col(db.ConversationMessage.sender_participant_id).in_(participant_ids)
)
if message_types is not None:
select_query = select_query.where(
col(db.ConversationMessage.message_type).in_([t.value for t in message_types])
)
if before is not None:
boundary = (
await session.exec(
query.select_conversation_messages_for(principal=principal).where(
db.ConversationMessage.message_id == before
)
)
).one_or_none()
if boundary is not None:
select_query = select_query.where(db.ConversationMessage.sequence < boundary.sequence)
if after is not None:
boundary = (
await session.exec(
query.select_conversation_messages_for(principal=principal).where(
db.ConversationMessage.message_id == after
)
)
).one_or_none()
if boundary is not None:
select_query = select_query.where(db.ConversationMessage.sequence > boundary.sequence)
messages = list(
(
await session.exec(select_query.order_by(col(db.ConversationMessage.sequence).desc()).limit(limit))
).all()
)
messages.reverse()
return convert.conversation_message_list_from_db(messages)
async def delete_message(
self,
conversation_id: uuid.UUID,
message_id: uuid.UUID,
user_principal: auth.UserPrincipal,
) -> None:
async with self._get_session() as session:
message = (
await session.exec(
query.select_conversation_messages_for(principal=user_principal).where(
db.ConversationMessage.conversation_id == conversation_id,
db.ConversationMessage.message_id == message_id,
or_(
db.Conversation.owner_id == user_principal.user_id,
db.ConversationMessage.sender_participant_id == user_principal.user_id,
and_(
col(db.UserParticipant.active_participant).is_(True),
db.UserParticipant.conversation_permission == "read_write",
),
),
)
)
).one_or_none()
if message is None:
raise exceptions.NotFoundError()
message_response = convert.conversation_message_from_db(message, has_debug=False)
await session.delete(message)
await session.commit()
await self._notify_event(
ConversationEventQueueItem(
event=ConversationEvent(
conversation_id=conversation_id,
event=ConversationEventType.message_deleted,
data={
"message": message_response.model_dump(),
},
),
)
)
```