This is page 97 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/tests/test_workbench_service.py:
--------------------------------------------------------------------------------
```python
import asyncio
import datetime
import io
import json
import logging
import re
import time
import uuid
from unittest.mock import AsyncMock, Mock
import httpx
import openai_client
import pytest
import semantic_workbench_api_model.assistant_model as api_model
import semantic_workbench_service
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import HttpUrl
from pytest_httpx import HTTPXMock
from semantic_workbench_api_model import workbench_model, workbench_service_client
from .types import MockUser
def test_service_init(workbench_service: FastAPI):
with TestClient(app=workbench_service):
pass
id_segment = "[0-9a-f-]+"
def register_assistant_service(client: TestClient) -> workbench_model.AssistantServiceRegistration:
new_registration = workbench_model.NewAssistantServiceRegistration(
assistant_service_id=uuid.uuid4().hex,
name="test-assistant-service",
description="",
)
http_response = client.post("/assistant-service-registrations", json=new_registration.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
registration = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
update_with_url = workbench_model.UpdateAssistantServiceRegistrationUrl(
name=new_registration.name,
description=new_registration.description,
url=HttpUrl("http://testassistantservice"),
online_expires_in_seconds=60,
)
http_response = client.put(
f"/assistant-service-registrations/{new_registration.assistant_service_id}",
json=update_with_url.model_dump(mode="json"),
headers=workbench_service_client.AssistantServiceRequestHeaders(
assistant_service_id=registration.assistant_service_id,
api_key=registration.api_key or "",
).to_headers(),
)
assert httpx.codes.is_success(http_response.status_code)
return registration
def test_create_assistant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
new_assistant = workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
metadata={"test": "value"},
)
http_response = client.post("/assistants", json=new_assistant.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
assistant_response = workbench_model.Assistant.model_validate(http_response.json())
assert assistant_response.name == new_assistant.name
assert assistant_response.assistant_service_id == new_assistant.assistant_service_id
assert assistant_response.metadata == new_assistant.metadata
def test_create_assistant_request_failure(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_exception(httpx.NetworkError("test error"))
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
new_assistant = workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
image="pass an image to circumvent the request to the assistant service to get one",
metadata={"test": "value"},
)
http_response = client.post("/assistants", json=new_assistant.model_dump(mode="json"))
assert http_response.status_code == httpx.codes.FAILED_DEPENDENCY
response_body = http_response.json()
assert "detail" in response_body
assert re.match(
r"Failed to connect to assistant at url http://testassistantservice/[0-9a-f-]{36}; NetworkError: test"
r" error",
response_body["detail"],
)
def exclude_system_keys(metadata: dict) -> dict:
"""Omit system metadata from the given metadata dictionary."""
return {k: v for k, v in metadata.items() if not k.startswith("__")}
def test_create_conversation(workbench_service: FastAPI, test_user: MockUser):
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_conversation = workbench_model.NewConversation(title="test", metadata={"test": "value"})
http_response = client.post("/conversations", json=new_conversation.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert conversation_response.title == new_conversation.title
assert exclude_system_keys(conversation_response.metadata) == new_conversation.metadata
http_response = client.get(f"/conversations/{conversation_response.id}")
assert httpx.codes.is_success(http_response.status_code)
get_conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert get_conversation_response.title == new_conversation.title
assert exclude_system_keys(get_conversation_response.metadata) == new_conversation.metadata
class AsyncContextManagerMock:
def __init__(self, mock: Mock) -> None:
self.mock = mock
async def __aenter__(self):
"""Enter async context manager."""
return self.mock
async def __aexit__(self, exc_type, exc, tb):
"""Exit async context manager."""
pass
def test_create_conversation_and_retitle(
workbench_service: FastAPI, test_user: MockUser, monkeypatch: pytest.MonkeyPatch
):
from semantic_workbench_service.controller.conversation import ConversationTitleResponse
mock_parsed_choice = Mock()
mock_parsed_choice.message.parsed = ConversationTitleResponse(title="A sweet title")
mock_parsed_completion = Mock()
mock_parsed_completion.choices = [mock_parsed_choice]
mock_client = Mock()
mock_client.beta.chat.completions.parse = AsyncMock()
mock_client.beta.chat.completions.parse.return_value = mock_parsed_completion
mock_create_client = Mock(spec=openai_client.create_client)
mock_create_client.return_value = AsyncContextManagerMock(mock_client)
monkeypatch.setattr(openai_client, "create_client", mock_create_client)
monkeypatch.setattr(semantic_workbench_service.settings.service, "azure_openai_endpoint", "https://something/")
monkeypatch.setattr(semantic_workbench_service.settings.service, "azure_openai_deployment", "something")
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_conversation = workbench_model.NewConversation(metadata={"test": "value"})
http_response = client.post("/conversations", json=new_conversation.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert conversation_response.title == "New Conversation"
http_response = client.get(f"/conversations/{conversation_response.id}")
assert httpx.codes.is_success(http_response.status_code)
get_conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert get_conversation_response.title == new_conversation.title
new_message = workbench_model.NewConversationMessage(content="hi")
http_response = client.post(
f"/conversations/{conversation_response.id}/messages", json=new_message.model_dump(mode="json")
)
assert httpx.codes.is_success(http_response.status_code)
for _ in range(10):
http_response = client.get(f"/conversations/{conversation_response.id}")
assert httpx.codes.is_success(http_response.status_code)
get_conversation_response = workbench_model.Conversation.model_validate(http_response.json())
if get_conversation_response.title != "New Conversation":
break
time.sleep(0.1)
get_conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert get_conversation_response.title == "A sweet title"
def test_create_update_conversation(workbench_service: FastAPI, test_user: MockUser):
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_conversation = workbench_model.NewConversation(title="test-conversation", metadata={"test": "value"})
http_response = client.post("/conversations", json=new_conversation.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
updated_title = f"new-title{uuid.uuid4()}"
updated_metadata = {"test": uuid.uuid4().hex}
http_response = client.patch(
f"/conversations/{conversation_response.id}",
json=workbench_model.UpdateConversation(title=updated_title, metadata=updated_metadata).model_dump(
mode="json",
),
)
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_response.id}")
assert httpx.codes.is_success(http_response.status_code)
get_conversation_response = workbench_model.Conversation.model_validate(http_response.json())
assert get_conversation_response.title == updated_title
assert exclude_system_keys(get_conversation_response.metadata) == updated_metadata
def test_create_assistant_add_to_conversation(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant = workbench_model.Assistant.model_validate(http_response.json())
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation = workbench_model.Conversation.model_validate(http_response.json())
http_response = client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/assistants/{assistant.id}/conversations")
assert httpx.codes.is_success(http_response.status_code)
assistant_conversations = workbench_model.ConversationList.model_validate(http_response.json())
assert len(assistant_conversations.conversations) == 1
assert assistant_conversations.conversations[0].id == conversation.id
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_assistant_add_to_conversation_delete_assistant_retains_participant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="DELETE",
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant = workbench_model.Assistant.model_validate(http_response.json())
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation = workbench_model.Conversation.model_validate(http_response.json())
http_response = client.put(f"/conversations/{conversation.id}/participants/{assistant.id}", json={})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation.id}/participants")
assert httpx.codes.is_success(http_response.status_code)
participants = workbench_model.ConversationParticipantList.model_validate(http_response.json())
assert len(participants.participants) == 2
assert {p.id for p in participants.participants} == {test_user.id, str(assistant.id)}
assistant_participant = next(p for p in participants.participants if p.id == str(assistant.id))
assert assistant_participant.name == assistant.name
assert assistant_participant.image == assistant.image
assert assistant_participant.active_participant is True
assert assistant_participant.online is True
# update assistant and verify that the participant attributes are updated
http_response = client.patch(
f"/assistants/{assistant.id}",
json=workbench_model.UpdateAssistant(name="new-name", image="foo").model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant = workbench_model.Assistant.model_validate(http_response.json())
assert assistant.name == "new-name"
assert assistant.image == "foo"
http_response = client.get(f"/conversations/{conversation.id}/participants")
assert httpx.codes.is_success(http_response.status_code)
participants = workbench_model.ConversationParticipantList.model_validate(http_response.json())
assert len(participants.participants) == 2
assert {p.id for p in participants.participants} == {test_user.id, str(assistant.id)}
assistant_participant = next(p for p in participants.participants if p.id == str(assistant.id))
assert assistant_participant.name == assistant.name
assert assistant_participant.image == assistant.image
assert assistant_participant.active_participant is True
assert assistant_participant.online is True
# delete assistant and verify that the participant is still in the conversation
http_response = client.delete(f"/assistants/{assistant.id}")
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation.id}/participants", params={"include_inactive": True})
assert httpx.codes.is_success(http_response.status_code)
participants = workbench_model.ConversationParticipantList.model_validate(http_response.json())
assert len(participants.participants) == 2
assert {p.id for p in participants.participants} == {test_user.id, str(assistant.id)}
assistant_participant = next(p for p in participants.participants if p.id == str(assistant.id))
assert assistant_participant.name == assistant.name
assert assistant_participant.image == assistant.image
assert assistant_participant.active_participant is False
assert assistant_participant.online is False
def test_create_get_assistant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
logging.info("response: %s", assistant_response)
assert "id" in assistant_response
assistant_id = assistant_response["id"]
http_response = client.get(f"/assistants/{assistant_id}")
assert httpx.codes.is_success(http_response.status_code)
assert http_response.json() == assistant_response
http_response = client.get("/assistants")
assert httpx.codes.is_success(http_response.status_code)
assistants_response = http_response.json()
assert "assistants" in assistants_response
assert assistants_response["assistants"] == [assistant_response]
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_update_assistant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
test_user_2: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
metadata={"test": "value"},
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
logging.info("response: %s", assistant_response)
assert "id" in assistant_response
assistant_id = assistant_response["id"]
updated_name = f"new-name{uuid.uuid4()}"
updated_metadata = {"test": uuid.uuid4().hex}
http_response = client.patch(
f"/assistants/{assistant_id}",
json=workbench_model.UpdateAssistant(name=updated_name, metadata=updated_metadata).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/assistants/{assistant_id}")
assert httpx.codes.is_success(http_response.status_code)
assistants_response = workbench_model.Assistant.model_validate(http_response.json())
assert assistants_response.name == updated_name
assert assistants_response.metadata == updated_metadata
# ensure another user cannot update
http_response = client.patch(
f"/assistants/{assistant_id}",
json=workbench_model.UpdateAssistant(name=updated_name, metadata=updated_metadata).model_dump(mode="json"),
headers=test_user_2.authorization_headers,
)
assert httpx.codes.is_client_error(http_response.status_code)
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_delete_assistant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
test_user_2: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="DELETE",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="DELETE",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
logging.info("response: %s", assistant_response)
assert "id" in assistant_response
assistant_id = assistant_response["id"]
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
http_response = client.put(f"/conversations/{conversation_id}/participants/{assistant_id}", json={})
assert httpx.codes.is_success(http_response.status_code)
payload = {"content": "hello"}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
assert httpx.codes.is_success(http_response.status_code)
# ensure another user cannot delete
http_response = client.delete(f"/assistants/{assistant_id}", headers=test_user_2.authorization_headers)
assert httpx.codes.is_client_error(http_response.status_code)
http_response = client.delete(f"/assistants/{assistant_id}")
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/assistants/{assistant_id}")
assert http_response.status_code == httpx.codes.NOT_FOUND
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_assistant_update_participant(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
assistant_id = http_response.json()["id"]
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
http_response = client.put(f"/conversations/{conversation_id}/participants/{assistant_id}", json={})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_id}/participants")
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
participants_response = http_response.json()
assert "participants" in participants_response
participants = participants_response["participants"]
assert len(participants) == 2
expected_participant_ids = {test_user.id, assistant_id}
participant_ids = {p["id"] for p in participants}
assert participant_ids == expected_participant_ids
http_response = client.get(f"/conversations/{conversation_id}/participants/{test_user.id}")
assert httpx.codes.is_success(http_response.status_code)
my_id_participant = http_response.json()
http_response = client.get(f"/conversations/{conversation_id}/participants/me")
assert httpx.codes.is_success(http_response.status_code)
me_participant = http_response.json()
assert my_id_participant == me_participant
http_response = client.patch(f"/conversations/{conversation_id}/participants/me", json={"status": "testing"})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_id}/participants/me")
assert httpx.codes.is_success(http_response.status_code)
updated_me_participant = http_response.json()
assert updated_me_participant["status"] == "testing"
me_timestamp = datetime.datetime.fromisoformat(me_participant["status_updated_timestamp"])
updated_timestamp = datetime.datetime.fromisoformat(updated_me_participant["status_updated_timestamp"])
assert updated_timestamp > me_timestamp
@pytest.mark.parametrize("message_type", ["command", "log", "note", "notice"])
def test_create_conversation_send_nonchat_message(
workbench_service: FastAPI,
test_user: MockUser,
message_type: str,
):
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
# create a chat message that should not be returned
message_content = "message of type chat"
payload = {"message_type": "chat", "content_type": "text/plain", "content": message_content}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
assert httpx.codes.is_success(http_response.status_code)
message_content = f"message of type {message_type}"
payload = {"message_type": message_type, "content_type": "text/plain", "content": message_content}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"message_type": message_type})
assert httpx.codes.is_success(http_response.status_code)
messages_response = http_response.json()
assert "messages" in messages_response
messages = messages_response["messages"]
assert len(messages) == 1
message = messages[0]
assert message["content"] == message_content
assert message["sender"]["participant_id"] == test_user.id
assert message["message_type"] == message_type
def test_create_conversation_send_user_message(workbench_service: FastAPI, test_user: MockUser):
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation = workbench_model.Conversation.model_validate(http_response.json())
conversation_id = conversation.id
assert conversation.latest_message is None
payload = {"content": "hello"}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
message = workbench_model.ConversationMessage.model_validate(http_response.json())
message_id = message.id
assert message.has_debug_data is False
http_response = client.get(f"/conversations/{conversation_id}/messages")
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 1
message = messages.messages[0]
assert message.content == "hello"
assert message.sender.participant_id == test_user.id
http_response = client.get(f"/conversations/{conversation_id}/messages/{message_id}")
assert httpx.codes.is_success(http_response.status_code)
message = workbench_model.ConversationMessage.model_validate(http_response.json())
assert message.content == "hello"
assert message.sender.participant_id == test_user.id
# send another chat message, with debug
payload = {
"content": "hello again",
"metadata": {"debug": {"key1": "value1"}},
"debug_data": {"key2": "value2"},
}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
message = workbench_model.ConversationMessage.model_validate(http_response.json())
message_two_id = message.id
# debug should be stripped out
assert message.metadata == {}
assert message.has_debug_data is True
http_response = client.get(f"/conversations/{conversation_id}/messages/{message_two_id}/debug_data")
assert httpx.codes.is_success(http_response.status_code)
message = workbench_model.ConversationMessageDebug.model_validate(http_response.json())
assert message.debug_data == {"key1": "value1", "key2": "value2"}
# send a log message
payload = {"content": "hello again", "message_type": "log"}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
message = workbench_model.ConversationMessage.model_validate(http_response.json())
message_log_id = message.id
# get all messages
http_response = client.get(f"/conversations/{conversation_id}/messages")
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 3
message = messages.messages[0]
assert message.content == "hello"
assert message.sender.participant_id == test_user.id
assert message.id == message_id
message = messages.messages[1]
assert message.content == "hello again"
assert message.sender.participant_id == test_user.id
assert message.id == message_two_id
message = messages.messages[2]
assert message.content == "hello again"
assert message.sender.participant_id == test_user.id
assert message.id == message_log_id
# limit messages
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"limit": 1})
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 1
message = messages.messages[0]
assert message.id == message_log_id
# get messages before
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"before": str(message_two_id)})
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 1
message = messages.messages[0]
assert message.id == message_id
# get messages after
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"after": str(message_id)})
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 2
message = messages.messages[0]
assert message.id == message_two_id
message = messages.messages[1]
assert message.id == message_log_id
# get messages by type
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"message_type": "chat"})
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 2
http_response = client.get(f"/conversations/{conversation_id}/messages", params={"message_type": "log"})
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 1
http_response = client.get(
f"/conversations/{conversation_id}/messages",
params={"message_type": ["chat", "log"]},
)
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 3
# check latest chat message in conversation (chat is default)
http_response = client.get(f"/conversations/{conversation_id}")
assert httpx.codes.is_success(http_response.status_code)
conversation = workbench_model.Conversation.model_validate(http_response.json())
assert conversation.latest_message is not None
assert conversation.latest_message.id == message_two_id
# check latest log message in conversation
http_response = client.get(f"/conversations/{conversation_id}", params={"latest_message_type": ["log"]})
assert httpx.codes.is_success(http_response.status_code)
conversation = workbench_model.Conversation.model_validate(http_response.json())
assert conversation.latest_message is not None
assert conversation.latest_message.id == message_log_id
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_assistant_send_assistant_message(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
assistant_response = http_response.json()
assistant_id = assistant_response["id"]
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
http_response = client.put(f"/conversations/{conversation_id}/participants/{assistant_id}", json={})
assert httpx.codes.is_success(http_response.status_code)
payload = {"content": "hello", "metadata": {"assistant_id": assistant_id, "generated_by": "test"}}
assistant_headers = {
**workbench_service_client.AssistantServiceRequestHeaders(
assistant_service_id=registration.assistant_service_id,
api_key=registration.api_key or "",
).to_headers(),
**workbench_service_client.AssistantRequestHeaders(
assistant_id=assistant_id,
).to_headers(),
}
http_response = client.post(
f"/conversations/{conversation_id}/messages",
json=payload,
headers=assistant_headers,
)
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_id}/messages")
assert httpx.codes.is_success(http_response.status_code)
messages_response = http_response.json()
assert "messages" in messages_response
messages = messages_response["messages"]
assert len(messages) == 1
message = messages[0]
assert message["content"] == "hello"
assert message["sender"]["participant_id"] == assistant_id
assert message["metadata"] == {"assistant_id": assistant_id, "generated_by": "test"}
def test_create_conversation_write_read_delete_file(
workbench_service: FastAPI,
test_user: MockUser,
):
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
http_response = client.get(f"/conversations/{conversation_id}/files")
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
files = http_response.json()["files"]
assert len(files) == 0
# write 3 files
payload = [
("files", ("test.txt", "hello world\n", "text/plain")),
("files", ("path1/path2/test.html", "<html><body></body></html>\n", "text/html")),
("files", ("path1/path2/test.bin", bytes(range(ord("a"), ord("f"))), "application/octet-stream")),
]
http_response = client.put(
f"/conversations/{conversation_id}/files",
files=payload,
# one of them has metadata
data={"metadata": json.dumps({"path1/path2/test.bin": {"generated_by": "test"}})},
)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
files = http_response.json()["files"]
assert [f["filename"] for f in files] == ["test.txt", "path1/path2/test.html", "path1/path2/test.bin"]
assert files[2]["metadata"] == {"generated_by": "test"}
# get the file listing
http_response = client.get(f"/conversations/{conversation_id}/files")
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
files = http_response.json()["files"]
assert [f["filename"] for f in files] == ["path1/path2/test.bin", "path1/path2/test.html", "test.txt"]
# get files by prefix
http_response = client.get(f"/conversations/{conversation_id}/files", params={"prefix": "path1/path2"})
assert httpx.codes.is_success(http_response.status_code)
logging.info("response: %s", http_response.json())
files = http_response.json()["files"]
assert [f["filename"] for f in files] == ["path1/path2/test.bin", "path1/path2/test.html"]
# download a file
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt")
assert httpx.codes.is_success(http_response.status_code)
assert http_response.text == "hello world\n"
# download another file
http_response = client.get(f"/conversations/{conversation_id}/files/path1/path2/test.html")
assert httpx.codes.is_success(http_response.status_code)
assert http_response.text == "<html><body></body></html>\n"
# re-write test.txt
payload = [
("files", ("test.txt", "hello again\n", "text/plain")),
]
http_response = client.put(f"/conversations/{conversation_id}/files", files=payload)
assert httpx.codes.is_success(http_response.status_code)
# get all versions
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt/versions")
assert httpx.codes.is_success(http_response.status_code)
file_versions = http_response.json()
assert len(file_versions["versions"]) == 2
assert file_versions["versions"][0]["version"] == 1
assert file_versions["versions"][1]["version"] == 2
# get a single version
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt/versions", params={"version": 1})
assert httpx.codes.is_success(http_response.status_code)
file_versions = http_response.json()
assert len(file_versions["versions"]) == 1
assert file_versions["versions"][0]["version"] == 1
# get the file content for the current version
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt")
assert httpx.codes.is_success(http_response.status_code)
assert http_response.text == "hello again\n"
# get the file content for the prior version
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt", params={"version": 1})
assert httpx.codes.is_success(http_response.status_code)
assert http_response.text == "hello world\n"
# delete a file
http_response = client.delete(f"/conversations/{conversation_id}/files/test.txt")
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt")
assert http_response.status_code == httpx.codes.NOT_FOUND
http_response = client.get(f"/conversations/{conversation_id}/files/test.txt/versions")
assert http_response.status_code == httpx.codes.NOT_FOUND
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_assistant_export_import_data(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/export-data"),
method="GET",
json={"data": "assistant test export data"},
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/export-data"),
method="GET",
json={"data": "conversation test export data"},
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
assistant_id = assistant_response["id"]
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id = conversation_response["id"]
http_response = client.put(f"/conversations/{conversation_id}/participants/{assistant_id}", json={})
assert httpx.codes.is_success(http_response.status_code)
payload = {"content": "hello"}
http_response = client.post(f"/conversations/{conversation_id}/messages", json=payload)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get(f"/assistants/{assistant_id}/export")
assert httpx.codes.is_success(http_response.status_code)
assert http_response.headers["content-type"] == "application/zip"
assert "content-length" in http_response.headers
assert int(http_response.headers["content-length"]) > 0
logging.info("response: %s", http_response.content)
file_io = io.BytesIO(http_response.content)
for import_number in range(1, 3):
http_response = client.post("/conversations/import", files={"from_export": file_io})
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get("/assistants")
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistants_response = http_response.json()
assert "assistants" in assistants_response
assistant_count = len(assistants_response["assistants"])
assert assistant_count == 1
for index, assistant in enumerate(assistants_response["assistants"]):
assert assistant["name"] == "test-assistant"
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_assistant_conversations_export_import_conversations(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
) -> None:
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/export-data"),
method="GET",
json={"data": "assistant test export data"},
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/export-data"),
method="GET",
json={"data": "conversation test export data"},
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant-1",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
assistant_id_1 = assistant_response["id"]
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant-2",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistant_response = http_response.json()
assistant_id_2 = assistant_response["id"]
http_response = client.post("/conversations", json={"title": "test-conversation-1"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id_1 = conversation_response["id"]
http_response = client.post("/conversations", json={"title": "test-conversation-2"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = http_response.json()
conversation_id_2 = conversation_response["id"]
# both assistants are in conversation-1
http_response = client.put(f"/conversations/{conversation_id_1}/participants/{assistant_id_1}", json={})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.put(f"/conversations/{conversation_id_1}/participants/{assistant_id_2}", json={})
assert httpx.codes.is_success(http_response.status_code)
# only assistant-1 is in conversation-2
http_response = client.put(f"/conversations/{conversation_id_2}/participants/{assistant_id_1}", json={})
assert httpx.codes.is_success(http_response.status_code)
payload = {"content": "hello", "debug_data": {"key": "value"}}
http_response = client.post(f"/conversations/{conversation_id_1}/messages", json=payload)
assert httpx.codes.is_success(http_response.status_code)
http_response = client.post(f"/conversations/{conversation_id_2}/messages", json=payload)
assert httpx.codes.is_success(http_response.status_code)
# export both conversations
http_response = client.get("/conversations/export", params={"id": [conversation_id_1, conversation_id_2]})
assert httpx.codes.is_success(http_response.status_code)
assert http_response.headers["content-type"] == "application/zip"
assert "content-length" in http_response.headers
assert int(http_response.headers["content-length"]) > 0
logging.info("response: %s", http_response.content)
file_io = io.BytesIO(http_response.content)
for _ in range(1, 3):
http_response = client.post("/conversations/import", files={"from_export": file_io})
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get("/assistants")
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistants_response = http_response.json()
assert "assistants" in assistants_response
assistant_count = len(assistants_response["assistants"])
assert assistant_count == 2
http_response = client.get("/assistants")
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
assistants = workbench_model.AssistantList.model_validate(http_response.json())
assistants.assistants = sorted(assistants.assistants, key=lambda a: a.name)
assert len(assistants.assistants) == 2
assert assistants.assistants[0].name == "test-assistant-1"
assert assistants.assistants[1].name == "test-assistant-2"
http_response = client.get("/conversations")
logging.info("response: %s", http_response.json())
assert httpx.codes.is_success(http_response.status_code)
conversations = workbench_model.ConversationList.model_validate(http_response.json())
conversations.conversations = sorted(conversations.conversations, key=lambda c: c.title)
assert conversations.conversations[0].title == "test-conversation-1"
assert conversations.conversations[1].title == "test-conversation-1 (1)"
assert conversations.conversations[2].title == "test-conversation-1 (2)"
assert conversations.conversations[3].title == "test-conversation-2"
assert conversations.conversations[4].title == "test-conversation-2 (1)"
assert conversations.conversations[5].title == "test-conversation-2 (2)"
for conversation in conversations.conversations:
http_response = client.get(f"/conversations/{conversation.id}/messages")
assert httpx.codes.is_success(http_response.status_code)
messages = workbench_model.ConversationMessageList.model_validate(http_response.json())
assert len(messages.messages) == 1
message = messages.messages[0]
assert message.content == "hello"
assert message.sender.participant_id == test_user.id
assert message.has_debug_data is True
http_response = client.get(f"/conversations/{conversation.id}/messages/{message.id}/debug_data")
assert httpx.codes.is_success(http_response.status_code)
message_debug = workbench_model.ConversationMessageDebug.model_validate(http_response.json())
assert message_debug.debug_data == {"key": "value"}
def test_export_import_conversations_with_files(
workbench_service: FastAPI,
test_user: MockUser,
) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation-1"})
assert httpx.codes.is_success(http_response.status_code)
conversation_1 = workbench_model.Conversation.model_validate(http_response.json())
http_response = client.post("/conversations", json={"title": "test-conversation-2"})
assert httpx.codes.is_success(http_response.status_code)
conversation_2 = workbench_model.Conversation.model_validate(http_response.json())
for conversation in [conversation_1, conversation_2]:
payload = [
("files", ("test.txt", "hello world\n", "text/plain")),
("files", ("path1/path2/test.html", "<html><body></body></html>\n", "text/html")),
("files", ("path1/path2/test.bin", bytes(range(ord("a"), ord("f"))), "application/octet-stream")),
]
http_response = client.put(f"/conversations/{conversation.id}/files", files=payload)
assert httpx.codes.is_success(http_response.status_code)
file_list = workbench_model.FileList.model_validate(http_response.json())
assert len(file_list.files) == 3
http_response = client.get(
"/conversations/export", params={"id": [str(conversation_1.id), str(conversation_2.id)]}
)
assert httpx.codes.is_success(http_response.status_code)
exported_data = io.BytesIO(http_response.content)
for _ in range(1, 2):
http_response = client.post("/conversations/import", files={"from_export": exported_data})
assert httpx.codes.is_success(http_response.status_code)
import_result = workbench_model.ConversationImportResult.model_validate(http_response.json())
assert len(import_result.conversation_ids) == 2
for conversation_id in import_result.conversation_ids:
http_response = client.get(f"/conversations/{conversation_id}/files")
assert httpx.codes.is_success(http_response.status_code)
file_list = workbench_model.FileList.model_validate(http_response.json())
assert len(file_list.files) == 3
for file in file_list.files:
http_response = client.get(f"/conversations/{conversation_id}/files/{file.filename}")
assert httpx.codes.is_success(http_response.status_code)
match file.filename:
case "test.txt":
assert http_response.text == "hello world\n"
case "path1/path2/test.html":
assert http_response.text == "<html><body></body></html>\n"
case "path1/path2/test.bin":
assert http_response.content == bytes(range(ord("a"), ord("f")))
case _:
pytest.fail(f"unexpected file: {file.filename}")
@pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_create_conversations_get_participants(
workbench_service: FastAPI,
httpx_mock: HTTPXMock,
test_user: MockUser,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
new_conversation_response = api_model.ConversationResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}"),
method="PUT",
json=new_conversation_response.model_dump(),
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}/conversations/{id_segment}/events"),
method="POST",
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation-1"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
conversation_id_1 = conversation_response.id
http_response = client.post("/conversations", json={"title": "test-conversation-2"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
conversation_id_2 = conversation_response.id
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant-1",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = workbench_model.Assistant.model_validate(http_response.json())
assistant_id_1 = assistant_response.id
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant-2",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = workbench_model.Assistant.model_validate(http_response.json())
assistant_id_2 = assistant_response.id
# both assistants are in conversation-1
http_response = client.put(f"/conversations/{conversation_id_1}/participants/{assistant_id_1}", json={})
assert httpx.codes.is_success(http_response.status_code)
http_response = client.put(f"/conversations/{conversation_id_1}/participants/{assistant_id_2}", json={})
assert httpx.codes.is_success(http_response.status_code)
# only assistant-1 is in conversation-2
http_response = client.put(f"/conversations/{conversation_id_2}/participants/{assistant_id_1}", json={})
assert httpx.codes.is_success(http_response.status_code)
for conversation_id, participant_ids in {
conversation_id_1: {str(assistant_id_1), str(assistant_id_2), test_user.id},
conversation_id_2: {str(assistant_id_1), test_user.id},
}.items():
http_response = client.get(f"/conversations/{conversation_id}/participants")
assert httpx.codes.is_success(http_response.status_code)
participants_response = workbench_model.ConversationParticipantList.model_validate(http_response.json())
assert {p.id for p in participants_response.participants} == participant_ids
for assistant_id, conversation_ids in {
assistant_id_1: {conversation_id_1, conversation_id_2},
assistant_id_2: {conversation_id_1},
}.items():
http_response = client.get(f"/assistants/{assistant_id}/conversations")
assert httpx.codes.is_success(http_response.status_code)
conversations_response = workbench_model.ConversationList.model_validate(http_response.json())
assert {c.id for c in conversations_response.conversations} == conversation_ids
@pytest.mark.parametrize(
"url_template",
[
"/conversations/{conversation_id}",
"/conversations/{conversation_id}/messages",
"/conversations/{conversation_id}/participants",
],
)
def test_conversation_not_visible_to_non_participants(
workbench_service: FastAPI,
test_user: MockUser,
test_user_2: MockUser,
httpx_mock: HTTPXMock,
url_template: str,
):
httpx_mock.add_response(
url="http://testassistantservice/",
method="GET",
json=api_model.ServiceInfoModel(assistant_service_id="", name="", templates=[], metadata={}).model_dump(
mode="json"
),
)
new_assistant_response = api_model.AssistantResponseModel(
id="123",
)
httpx_mock.add_response(
url=re.compile(f"http://testassistantservice/{id_segment}"),
method="PUT",
json=new_assistant_response.model_dump(),
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
http_response = client.post("/conversations", json={"title": "test-conversation"})
assert httpx.codes.is_success(http_response.status_code)
conversation_response = workbench_model.Conversation.model_validate(http_response.json())
conversation_id = conversation_response.id
registration = register_assistant_service(client)
http_response = client.post(
"/assistants",
json=workbench_model.NewAssistant(
name="test-assistant",
assistant_service_id=registration.assistant_service_id,
).model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_response = workbench_model.Assistant.model_validate(http_response.json())
# ensure user 2 cannot make get request
http_response = client.get(
url_template.format(conversation_id=conversation_id),
headers=test_user_2.authorization_headers,
)
assert http_response.status_code == httpx.codes.NOT_FOUND
# ensure assistant request always returns 404
assistant_headers = {
**workbench_service_client.AssistantServiceRequestHeaders(
assistant_service_id=registration.assistant_service_id,
api_key=registration.api_key or "",
).to_headers(),
**workbench_service_client.AssistantRequestHeaders(
assistant_id=assistant_response.id,
).to_headers(),
}
http_response = client.get(url_template.format(conversation_id=conversation_id), headers=assistant_headers)
assert http_response.status_code == httpx.codes.NOT_FOUND
def test_create_assistant_service_registration(workbench_service: FastAPI, test_user: MockUser) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-name",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
created_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert created_assistant_service.name == new_assistant_service.name
assert created_assistant_service.description == new_assistant_service.description
assert created_assistant_service.created_by_user_id == test_user.id
assert created_assistant_service.api_key is not None
def test_create_get_assistant_service_registration(workbench_service: FastAPI, test_user: MockUser) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-name",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
# get single registration
http_response = client.get(f"/assistant-service-registrations/{new_assistant_service.assistant_service_id}")
assert httpx.codes.is_success(http_response.status_code)
assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert assistant_service.name == new_assistant_service.name
assert assistant_service.description == new_assistant_service.description
assert assistant_service.created_by_user_id == test_user.id
# get on single registration returns a mask API key
assert assistant_service.api_key is not None
assert assistant_service.api_key.endswith("*" * 10)
# get all registrations
http_response = client.get("/assistant-service-registrations")
assert httpx.codes.is_success(http_response.status_code)
retrieved_assistant_services = workbench_model.AssistantServiceRegistrationList.model_validate(
http_response.json(),
)
assert len(retrieved_assistant_services.assistant_service_registrations) == 1
assistant_service = retrieved_assistant_services.assistant_service_registrations[0]
assert assistant_service.name == new_assistant_service.name
assert assistant_service.description == new_assistant_service.description
assert assistant_service.created_by_user_id == test_user.id
assert assistant_service.api_key is None
# get registrations owned by user
http_response = client.get("/assistant-service-registrations", params={"owner_id": "me"})
assert httpx.codes.is_success(http_response.status_code)
retrieved_assistant_services = workbench_model.AssistantServiceRegistrationList.model_validate(
http_response.json(),
)
assert len(retrieved_assistant_services.assistant_service_registrations) == 1
assistant_service = retrieved_assistant_services.assistant_service_registrations[0]
assert assistant_service.name == new_assistant_service.name
assert assistant_service.description == new_assistant_service.description
assert assistant_service.created_by_user_id == test_user.id
assert assistant_service.api_key is None
def test_create_update_assistant_service_registration(workbench_service: FastAPI, test_user: MockUser) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-nam",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
update_assistant_service = workbench_model.UpdateAssistantServiceRegistration(
name="updated-assistant-service",
description="updated description",
)
http_response = client.patch(
f"/assistant-service-registrations/{assistant_service.assistant_service_id}",
json=update_assistant_service.model_dump(mode="json", exclude_unset=True),
)
assert httpx.codes.is_success(http_response.status_code)
updated_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert updated_assistant_service.name == update_assistant_service.name
assert updated_assistant_service.description == update_assistant_service.description
assert updated_assistant_service.api_key is None
def test_create_assistant_service_registration_reset_api_key(workbench_service: FastAPI, test_user: MockUser) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-name",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
created_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert created_assistant_service.api_key is not None
http_response = client.post(
f"/assistant-service-registrations/{created_assistant_service.assistant_service_id}/api-key",
)
assert httpx.codes.is_success(http_response.status_code)
reset_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert reset_assistant_service.api_key is not None
# NOTE: the api key will not change because the test ApiKeyStore is used
def test_create_delete_assistant_service_registration(workbench_service: FastAPI, test_user: MockUser) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-name",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
created_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
http_response = client.delete(
f"/assistant-service-registrations/{created_assistant_service.assistant_service_id}",
)
assert httpx.codes.is_success(http_response.status_code)
http_response = client.get("/assistant-service-registrations")
assert httpx.codes.is_success(http_response.status_code)
retrieved_assistant_services = workbench_model.AssistantServiceRegistrationList.model_validate(
http_response.json(),
)
assert len(retrieved_assistant_services.assistant_service_registrations) == 0
async def test_create_update_assistant_service_registration_url(
workbench_service: FastAPI,
test_user: MockUser,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# force continuous checks for assistants going offline
monkeypatch.setattr(
semantic_workbench_service.settings.service,
"assistant_service_online_check_interval_seconds",
0.1,
)
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_assistant_service = workbench_model.NewAssistantServiceRegistration(
assistant_service_id="test-assistant-service-id",
name="test-assistant-service-nam",
description="test description",
)
http_response = client.post(
"/assistant-service-registrations",
json=new_assistant_service.model_dump(mode="json"),
)
assert httpx.codes.is_success(http_response.status_code)
assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
update_assistant_service = workbench_model.UpdateAssistantServiceRegistrationUrl(
name="updated-assistant-service",
description="updated description",
url=HttpUrl("https://example.com"),
online_expires_in_seconds=0,
)
http_response = client.put(
f"/assistant-service-registrations/{assistant_service.assistant_service_id}",
json=update_assistant_service.model_dump(mode="json", exclude_unset=True),
headers=workbench_service_client.AssistantServiceRequestHeaders(
assistant_service_id=assistant_service.assistant_service_id,
api_key=assistant_service.api_key or "",
).to_headers(),
)
assert httpx.codes.is_success(http_response.status_code)
updated_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert updated_assistant_service.api_key is None
assert updated_assistant_service.assistant_service_url == str(update_assistant_service.url)
assert updated_assistant_service.assistant_service_online is True
# give time for the assistant service online check to run
await asyncio.sleep(1.0)
# verify that when the url expires, the assistant service is reported as offline
http_response = client.get(f"/assistant-service-registrations/{assistant_service.assistant_service_id}")
assert httpx.codes.is_success(http_response.status_code)
retrieved_assistant_service = workbench_model.AssistantServiceRegistration.model_validate(http_response.json())
assert retrieved_assistant_service.assistant_service_online is False
@pytest.mark.parametrize(
("permission"),
[
workbench_model.ConversationPermission.read,
workbench_model.ConversationPermission.read_write,
],
)
async def test_create_redeem_delete_conversation_share(
workbench_service: FastAPI,
test_user: MockUser,
test_user_2: MockUser,
permission: workbench_model.ConversationPermission,
) -> None:
with TestClient(app=workbench_service, headers=test_user.authorization_headers) as client:
new_conversation = workbench_model.NewConversation(title="test-conversation")
http_response = client.post("/conversations", json=new_conversation.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
created_conversation = workbench_model.Conversation.model_validate(http_response.json())
new_conversation_share = workbench_model.NewConversationShare(
conversation_id=created_conversation.id,
label="share",
conversation_permission=permission,
)
http_response = client.post("/conversation-shares", json=new_conversation_share.model_dump(mode="json"))
assert httpx.codes.is_success(http_response.status_code)
created_conversation_share = workbench_model.ConversationShare.model_validate(http_response.json())
assert created_conversation_share.conversation_id == created_conversation.id
assert created_conversation_share.conversation_permission == permission
http_response = client.get(f"/conversation-shares/{created_conversation_share.id}")
assert httpx.codes.is_success(http_response.status_code)
retrieved_conversation_share = workbench_model.ConversationShare.model_validate(http_response.json())
assert retrieved_conversation_share == created_conversation_share
http_response = client.get("/conversation-shares")
assert httpx.codes.is_success(http_response.status_code)
retrieved_conversation_shares = workbench_model.ConversationShareList.model_validate(http_response.json())
assert retrieved_conversation_shares.conversation_shares == [created_conversation_share]
# redeem the conversation share with user-2
http_response = client.post(
f"/conversation-shares/{created_conversation_share.id}/redemptions",
headers=test_user_2.authorization_headers,
)
assert httpx.codes.is_success(http_response.status_code)
redemption = workbench_model.ConversationShareRedemption.model_validate(http_response.json())
assert redemption.redeemed_by_user.id == test_user_2.id
assert redemption.conversation_permission == permission
# ensure user-2 can retrieve the conversation
http_response = client.get(
f"/conversations/{created_conversation.id}", headers=test_user_2.authorization_headers
)
assert httpx.codes.is_success(http_response.status_code)
retrieved_conversation = workbench_model.Conversation.model_validate(http_response.json())
assert retrieved_conversation.id == created_conversation.id
# ensure user-2 can retrieve their participant
http_response = client.get(
f"/conversations/{created_conversation.id}/participants/me", headers=test_user_2.authorization_headers
)
participant = workbench_model.ConversationParticipant.model_validate(http_response.json())
assert participant.role == workbench_model.ParticipantRole.user
assert participant.conversation_id == created_conversation.id
assert participant.active_participant is True
assert participant.conversation_permission == permission
# delete the conversation share
http_response = client.delete(f"/conversation-shares/{created_conversation_share.id}")
assert httpx.codes.is_success(http_response.status_code)
# ensure user-2 can still retrieve the conversation
http_response = client.get(
f"/conversations/{created_conversation.id}", headers=test_user_2.authorization_headers
)
assert httpx.codes.is_success(http_response.status_code)
# ensure user 2 can no longer redeem the conversation share
http_response = client.post(
"/conversation-shares/redeem",
headers=test_user_2.authorization_headers,
)
assert httpx.codes.is_client_error(http_response.status_code)
```