This is page 145 of 145. Use http://codebase.md/microsoft/semanticworkbench?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ ├── .vscode
│ │ └── settings.json
│ ├── devcontainer.json
│ ├── OPTIMIZING_FOR_CODESPACES.md
│ ├── POST_SETUP_README.md
│ └── README.md
├── .dockerignore
├── .gitattributes
├── .github
│ ├── policheck.yml
│ └── workflows
│ ├── assistants-codespace-assistant.yml
│ ├── assistants-document-assistant.yml
│ ├── assistants-explorer-assistant.yml
│ ├── assistants-guided-conversation-assistant.yml
│ ├── assistants-knowledge-transfer-assistant.yml
│ ├── assistants-navigator-assistant.yml
│ ├── assistants-project-assistant.yml
│ ├── assistants-prospector-assistant.yml
│ ├── assistants-skill-assistant.yml
│ ├── libraries.yml
│ ├── mcp-server-giphy.yml
│ ├── mcp-server-memory-filesystem-edit.yml
│ ├── mcp-server-memory-user-bio.yml
│ ├── mcp-server-memory-whiteboard.yml
│ ├── mcp-server-open-deep-research-clone.yml
│ ├── mcp-server-web-research.yml
│ ├── workbench-app.yml
│ └── workbench-service.yml
├── .gitignore
├── .multi-root-tools
│ ├── Makefile
│ └── README.md
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── ai_context
│ └── generated
│ ├── ASPIRE_ORCHESTRATOR.md
│ ├── ASSISTANT_CODESPACE.md
│ ├── ASSISTANT_DOCUMENT.md
│ ├── ASSISTANT_NAVIGATOR.md
│ ├── ASSISTANT_PROJECT.md
│ ├── ASSISTANT_PROSPECTOR.md
│ ├── ASSISTANTS_OTHER.md
│ ├── ASSISTANTS_OVERVIEW.md
│ ├── CONFIGURATION.md
│ ├── DOTNET_LIBRARIES.md
│ ├── EXAMPLES.md
│ ├── MCP_SERVERS.md
│ ├── PYTHON_LIBRARIES_AI_CLIENTS.md
│ ├── PYTHON_LIBRARIES_CORE.md
│ ├── PYTHON_LIBRARIES_EXTENSIONS.md
│ ├── PYTHON_LIBRARIES_SKILLS.md
│ ├── PYTHON_LIBRARIES_SPECIALIZED.md
│ ├── TOOLS.md
│ ├── WORKBENCH_FRONTEND.md
│ └── WORKBENCH_SERVICE.md
├── aspire-orchestrator
│ ├── .editorconfig
│ ├── Aspire.AppHost
│ │ ├── .gitignore
│ │ ├── appsettings.json
│ │ ├── Aspire.AppHost.csproj
│ │ ├── Program.cs
│ │ └── Properties
│ │ └── launchSettings.json
│ ├── Aspire.Extensions
│ │ ├── Aspire.Extensions.csproj
│ │ ├── Dashboard.cs
│ │ ├── DockerFileExtensions.cs
│ │ ├── PathNormalizer.cs
│ │ ├── UvAppHostingExtensions.cs
│ │ ├── UvAppResource.cs
│ │ ├── VirtualEnvironment.cs
│ │ └── WorkbenchServiceHostingExtensions.cs
│ ├── Aspire.ServiceDefaults
│ │ ├── Aspire.ServiceDefaults.csproj
│ │ └── Extensions.cs
│ ├── README.md
│ ├── run.sh
│ ├── SemanticWorkbench.Aspire.sln
│ └── SemanticWorkbench.Aspire.sln.DotSettings
├── assistants
│ ├── codespace-assistant
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ ├── icon_context_transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── models.py
│ │ │ │ ├── request_builder.py
│ │ │ │ ├── response.py
│ │ │ │ ├── step_handler.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── abbreviations.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ └── openai_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── card_content_context_transfer.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── codespace_assistant_info.md
│ │ │ │ ├── context_transfer_assistant_info.md
│ │ │ │ ├── guardrails_prompt.txt
│ │ │ │ ├── guidance_prompt_context_transfer.txt
│ │ │ │ ├── guidance_prompt.txt
│ │ │ │ ├── instruction_prompt_context_transfer.txt
│ │ │ │ └── instruction_prompt.txt
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── document-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── context_management
│ │ │ │ ├── __init__.py
│ │ │ │ └── inspector.py
│ │ │ ├── filesystem
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _convert.py
│ │ │ │ ├── _file_sources.py
│ │ │ │ ├── _filesystem.py
│ │ │ │ ├── _inspector.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _prompts.py
│ │ │ │ └── _tasks.py
│ │ │ ├── guidance
│ │ │ │ ├── __init__.py
│ │ │ │ ├── dynamic_ui_inspector.py
│ │ │ │ ├── guidance_config.py
│ │ │ │ ├── guidance_prompts.py
│ │ │ │ └── README.md
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── models.py
│ │ │ │ ├── prompts.py
│ │ │ │ ├── responder.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ ├── openai_utils.py
│ │ │ │ ├── tokens_tiktoken.py
│ │ │ │ └── workbench_messages.py
│ │ │ ├── text_includes
│ │ │ │ └── document_assistant_info.md
│ │ │ ├── types.py
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── assistant.code-workspace
│ │ ├── CLAUDE.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_convert.py
│ │ │ └── test_data
│ │ │ ├── blank_image.png
│ │ │ ├── Formatting Test.docx
│ │ │ ├── sample_data.csv
│ │ │ ├── sample_data.xlsx
│ │ │ ├── sample_page.html
│ │ │ ├── sample_presentation.pptx
│ │ │ └── simple_pdf.pdf
│ │ └── uv.lock
│ ├── explorer-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── model.py
│ │ │ │ ├── response_anthropic.py
│ │ │ │ ├── response_openai.py
│ │ │ │ └── response.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── guided-conversation-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agents
│ │ │ │ ├── guided_conversation
│ │ │ │ │ ├── config.py
│ │ │ │ │ ├── definition.py
│ │ │ │ │ └── definitions
│ │ │ │ │ ├── er_triage.py
│ │ │ │ │ ├── interview.py
│ │ │ │ │ ├── patient_intake.py
│ │ │ │ │ └── poem_feedback.py
│ │ │ │ └── guided_conversation_agent.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── knowledge-transfer-assistant
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agentic
│ │ │ │ ├── __init__.py
│ │ │ │ ├── analysis.py
│ │ │ │ ├── coordinator_support.py
│ │ │ │ └── team_welcome.py
│ │ │ ├── assets
│ │ │ │ ├── icon-knowledge-transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── assistant.py
│ │ │ ├── common.py
│ │ │ ├── config.py
│ │ │ ├── conversation_clients.py
│ │ │ ├── conversation_share_link.py
│ │ │ ├── data.py
│ │ │ ├── domain
│ │ │ │ ├── __init__.py
│ │ │ │ ├── audience_manager.py
│ │ │ │ ├── information_request_manager.py
│ │ │ │ ├── knowledge_brief_manager.py
│ │ │ │ ├── knowledge_digest_manager.py
│ │ │ │ ├── learning_objectives_manager.py
│ │ │ │ └── share_manager.py
│ │ │ ├── files.py
│ │ │ ├── logging.py
│ │ │ ├── notifications.py
│ │ │ ├── respond.py
│ │ │ ├── storage_models.py
│ │ │ ├── storage.py
│ │ │ ├── string_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── assistant_info.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── coordinator_instructions.txt
│ │ │ │ ├── coordinator_role.txt
│ │ │ │ ├── knowledge_digest_instructions.txt
│ │ │ │ ├── knowledge_digest_prompt.txt
│ │ │ │ ├── share_information_request_detection.txt
│ │ │ │ ├── team_instructions.txt
│ │ │ │ ├── team_role.txt
│ │ │ │ └── welcome_message_generation.txt
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── information_requests.py
│ │ │ │ ├── learning_objectives.py
│ │ │ │ ├── learning_outcomes.py
│ │ │ │ ├── progress_tracking.py
│ │ │ │ └── share_setup.py
│ │ │ ├── ui_tabs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── brief.py
│ │ │ │ ├── common.py
│ │ │ │ ├── debug.py
│ │ │ │ ├── learning.py
│ │ │ │ └── sharing.py
│ │ │ └── utils.py
│ │ ├── CLAUDE.md
│ │ ├── docs
│ │ │ ├── design
│ │ │ │ ├── actions.md
│ │ │ │ └── inference.md
│ │ │ ├── DEV_GUIDE.md
│ │ │ ├── how-kta-works.md
│ │ │ ├── JTBD.md
│ │ │ ├── knowledge-transfer-goals.md
│ │ │ ├── learning_assistance.md
│ │ │ ├── notable_claude_conversations
│ │ │ │ ├── clarifying_quad_modal_design.md
│ │ │ │ ├── CLAUDE_PROMPTS.md
│ │ │ │ ├── transfer_state.md
│ │ │ │ └── trying_the_context_agent.md
│ │ │ └── opportunities-of-knowledge-transfer.md
│ │ ├── knowledge-transfer-assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_artifact_loading.py
│ │ │ ├── test_inspector.py
│ │ │ ├── test_share_manager.py
│ │ │ ├── test_share_storage.py
│ │ │ ├── test_share_tools.py
│ │ │ └── test_team_mode.py
│ │ └── uv.lock
│ ├── Makefile
│ ├── navigator-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── assets
│ │ │ │ ├── card_content.md
│ │ │ │ └── icon.svg
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── helpers.py
│ │ │ ├── response
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion_handler.py
│ │ │ │ ├── completion_requestor.py
│ │ │ │ ├── local_tool
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── add_assistant_to_conversation.py
│ │ │ │ │ ├── list_assistant_services.py
│ │ │ │ │ └── model.py
│ │ │ │ ├── models.py
│ │ │ │ ├── prompt.py
│ │ │ │ ├── request_builder.py
│ │ │ │ ├── response.py
│ │ │ │ ├── step_handler.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── formatting_utils.py
│ │ │ │ ├── message_utils.py
│ │ │ │ ├── openai_utils.py
│ │ │ │ └── tools.py
│ │ │ ├── text_includes
│ │ │ │ ├── guardrails_prompt.md
│ │ │ │ ├── guidance_prompt.md
│ │ │ │ ├── instruction_prompt.md
│ │ │ │ ├── navigator_assistant_info.md
│ │ │ │ └── semantic_workbench_features.md
│ │ │ └── whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── _inspector.py
│ │ │ └── _whiteboard.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── project-assistant
│ │ ├── .cspell
│ │ │ └── custom-dictionary-workspace.txt
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agentic
│ │ │ │ ├── __init__.py
│ │ │ │ ├── act.py
│ │ │ │ ├── coordinator_next_action.py
│ │ │ │ ├── create_invitation.py
│ │ │ │ ├── detect_audience_and_takeaways.py
│ │ │ │ ├── detect_coordinator_actions.py
│ │ │ │ ├── detect_information_request_needs.py
│ │ │ │ ├── detect_knowledge_package_gaps.py
│ │ │ │ ├── focus.py
│ │ │ │ ├── respond.py
│ │ │ │ ├── team_welcome.py
│ │ │ │ └── update_digest.py
│ │ │ ├── assets
│ │ │ │ ├── icon-knowledge-transfer.svg
│ │ │ │ └── icon.svg
│ │ │ ├── assistant.py
│ │ │ ├── common.py
│ │ │ ├── config.py
│ │ │ ├── conversation_clients.py
│ │ │ ├── data.py
│ │ │ ├── domain
│ │ │ │ ├── __init__.py
│ │ │ │ ├── audience_manager.py
│ │ │ │ ├── conversation_preferences_manager.py
│ │ │ │ ├── information_request_manager.py
│ │ │ │ ├── knowledge_brief_manager.py
│ │ │ │ ├── knowledge_digest_manager.py
│ │ │ │ ├── learning_objectives_manager.py
│ │ │ │ ├── share_manager.py
│ │ │ │ ├── tasks_manager.py
│ │ │ │ └── transfer_manager.py
│ │ │ ├── errors.py
│ │ │ ├── files.py
│ │ │ ├── logging.py
│ │ │ ├── notifications.py
│ │ │ ├── prompt_utils.py
│ │ │ ├── storage.py
│ │ │ ├── string_utils.py
│ │ │ ├── text_includes
│ │ │ │ ├── actor_instructions.md
│ │ │ │ ├── assistant_info.md
│ │ │ │ ├── card_content.md
│ │ │ │ ├── coordinator_instructions copy.md
│ │ │ │ ├── coordinator_instructions.md
│ │ │ │ ├── create_invitation.md
│ │ │ │ ├── detect_audience.md
│ │ │ │ ├── detect_coordinator_actions.md
│ │ │ │ ├── detect_information_request_needs.md
│ │ │ │ ├── detect_knowledge_package_gaps.md
│ │ │ │ ├── focus.md
│ │ │ │ ├── knowledge_digest_instructions.txt
│ │ │ │ ├── team_instructions.txt
│ │ │ │ ├── to_do.md
│ │ │ │ ├── update_knowledge_brief.md
│ │ │ │ ├── update_knowledge_digest.md
│ │ │ │ └── welcome_message_generation.txt
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── conversation_preferences.py
│ │ │ │ ├── information_requests.py
│ │ │ │ ├── learning_objectives.py
│ │ │ │ ├── learning_outcomes.py
│ │ │ │ ├── progress_tracking.py
│ │ │ │ ├── share_setup.py
│ │ │ │ ├── system_reminders.py
│ │ │ │ ├── tasks.py
│ │ │ │ └── todo.py
│ │ │ ├── ui_tabs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── brief.py
│ │ │ │ ├── common.py
│ │ │ │ ├── debug.py
│ │ │ │ ├── learning.py
│ │ │ │ └── sharing.py
│ │ │ └── utils.py
│ │ ├── CLAUDE.md
│ │ ├── docs
│ │ │ ├── design
│ │ │ │ ├── actions.md
│ │ │ │ ├── control_options.md
│ │ │ │ ├── design.md
│ │ │ │ ├── inference.md
│ │ │ │ └── PXL_20250814_190140267.jpg
│ │ │ ├── DEV_GUIDE.md
│ │ │ ├── how-kta-works.md
│ │ │ ├── JTBD.md
│ │ │ ├── knowledge-transfer-goals.md
│ │ │ ├── learning_assistance.md
│ │ │ ├── notable_claude_conversations
│ │ │ │ ├── clarifying_quad_modal_design.md
│ │ │ │ ├── CLAUDE_PROMPTS.md
│ │ │ │ ├── transfer_state.md
│ │ │ │ └── trying_the_context_agent.md
│ │ │ └── opportunities-of-knowledge-transfer.md
│ │ ├── knowledge-transfer-assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_artifact_loading.py
│ │ │ ├── test_inspector.py
│ │ │ ├── test_share_manager.py
│ │ │ ├── test_share_storage.py
│ │ │ └── test_team_mode.py
│ │ └── uv.lock
│ ├── prospector-assistant
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── agents
│ │ │ │ ├── artifact_agent.py
│ │ │ │ ├── document
│ │ │ │ │ ├── config.py
│ │ │ │ │ ├── gc_draft_content_feedback_config.py
│ │ │ │ │ ├── gc_draft_outline_feedback_config.py
│ │ │ │ │ ├── guided_conversation.py
│ │ │ │ │ └── state.py
│ │ │ │ └── document_agent.py
│ │ │ ├── artifact_creation_extension
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _llm.py
│ │ │ │ ├── config.py
│ │ │ │ ├── document.py
│ │ │ │ ├── extension.py
│ │ │ │ ├── store.py
│ │ │ │ ├── test
│ │ │ │ │ ├── conftest.py
│ │ │ │ │ ├── evaluation.py
│ │ │ │ │ ├── test_completion_with_tools.py
│ │ │ │ │ └── test_extension.py
│ │ │ │ └── tools.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ ├── form_fill_extension
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── extension.py
│ │ │ │ ├── inspector.py
│ │ │ │ ├── state.py
│ │ │ │ └── steps
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _guided_conversation.py
│ │ │ │ ├── _llm.py
│ │ │ │ ├── acquire_form_step.py
│ │ │ │ ├── extract_form_fields_step.py
│ │ │ │ ├── fill_form_step.py
│ │ │ │ └── types.py
│ │ │ ├── helpers.py
│ │ │ ├── legacy.py
│ │ │ └── text_includes
│ │ │ ├── artifact_agent_enabled.md
│ │ │ ├── guardrails_prompt.txt
│ │ │ ├── guided_conversation_agent_enabled.md
│ │ │ └── skills_agent_enabled.md
│ │ ├── assistant.code-workspace
│ │ ├── gc_learnings
│ │ │ ├── gc_learnings.md
│ │ │ └── images
│ │ │ ├── gc_conversation_plan_fcn.png
│ │ │ ├── gc_conversation_plan_template.png
│ │ │ ├── gc_execute_plan_callstack.png
│ │ │ ├── gc_functions.png
│ │ │ ├── gc_generate_plan_callstack.png
│ │ │ ├── gc_get_resource_instructions.png
│ │ │ ├── gc_get_termination_instructions.png
│ │ │ ├── gc_kernel_arguments.png
│ │ │ ├── gc_plan_calls.png
│ │ │ ├── gc_termination_instructions.png
│ │ │ ├── sk_get_chat_message_contents.png
│ │ │ ├── sk_inner_get_chat_message_contents.png
│ │ │ ├── sk_send_request_prep.png
│ │ │ └── sk_send_request.png
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ └── skill-assistant
│ ├── .env.example
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── assistant
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── logging.py
│ │ ├── skill_assistant.py
│ │ ├── skill_engine_registry.py
│ │ ├── skill_event_mapper.py
│ │ ├── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ └── workbench_helpers.py
│ ├── assistant.code-workspace
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ ├── tests
│ │ └── test_setup.py
│ └── uv.lock
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── docs
│ ├── .vscode
│ │ └── settings.json
│ ├── ASSISTANT_CONFIG.md
│ ├── ASSISTANT_DEVELOPMENT_GUIDE.md
│ ├── CUSTOM_APP_REGISTRATION.md
│ ├── HOSTED_ASSISTANT_WITH_LOCAL_MCP_SERVERS.md
│ ├── images
│ │ ├── architecture-animation.gif
│ │ ├── configure_assistant.png
│ │ ├── conversation_canvas_open.png
│ │ ├── conversation_duplicate.png
│ │ ├── conversation_export.png
│ │ ├── conversation_share_dialog.png
│ │ ├── conversation_share_link.png
│ │ ├── dashboard_configured_view.png
│ │ ├── dashboard_view.png
│ │ ├── license_agreement.png
│ │ ├── message_bar.png
│ │ ├── message_inspection.png
│ │ ├── message_link.png
│ │ ├── new_prospector_assistant_dialog.png
│ │ ├── open_conversation_canvas.png
│ │ ├── prospector_example.png
│ │ ├── readme1.png
│ │ ├── readme2.png
│ │ ├── readme3.png
│ │ ├── rewind.png
│ │ ├── signin_page.png
│ │ └── splash_screen.png
│ ├── LOCAL_ASSISTANT_WITH_REMOTE_WORKBENCH.md
│ ├── SETUP_DEV_ENVIRONMENT.md
│ └── WORKBENCH_APP.md
├── examples
│ ├── dotnet
│ │ ├── .editorconfig
│ │ ├── dotnet-01-echo-bot
│ │ │ ├── appsettings.json
│ │ │ ├── dotnet-01-echo-bot.csproj
│ │ │ ├── MyAgent.cs
│ │ │ ├── MyAgentConfig.cs
│ │ │ ├── MyWorkbenchConnector.cs
│ │ │ ├── Program.cs
│ │ │ └── README.md
│ │ ├── dotnet-02-message-types-demo
│ │ │ ├── appsettings.json
│ │ │ ├── ConnectorExtensions.cs
│ │ │ ├── docs
│ │ │ │ ├── abc.png
│ │ │ │ ├── code.png
│ │ │ │ ├── config.png
│ │ │ │ ├── echo.png
│ │ │ │ ├── markdown.png
│ │ │ │ ├── mermaid.png
│ │ │ │ ├── reverse.png
│ │ │ │ └── safety-check.png
│ │ │ ├── dotnet-02-message-types-demo.csproj
│ │ │ ├── MyAgent.cs
│ │ │ ├── MyAgentConfig.cs
│ │ │ ├── MyWorkbenchConnector.cs
│ │ │ ├── Program.cs
│ │ │ └── README.md
│ │ └── dotnet-03-simple-chatbot
│ │ ├── appsettings.json
│ │ ├── ConnectorExtensions.cs
│ │ ├── dotnet-03-simple-chatbot.csproj
│ │ ├── MyAgent.cs
│ │ ├── MyAgentConfig.cs
│ │ ├── MyWorkbenchConnector.cs
│ │ ├── Program.cs
│ │ └── README.md
│ ├── Makefile
│ └── python
│ ├── python-01-echo-bot
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ └── config.py
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── python-02-simple-chatbot
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant
│ │ │ ├── __init__.py
│ │ │ ├── chat.py
│ │ │ ├── config.py
│ │ │ └── text_includes
│ │ │ └── guardrails_prompt.txt
│ │ ├── assistant.code-workspace
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ └── python-03-multimodel-chatbot
│ ├── .env.example
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── assistant
│ │ ├── __init__.py
│ │ ├── chat.py
│ │ ├── config.py
│ │ ├── model_adapters.py
│ │ └── text_includes
│ │ └── guardrails_prompt.txt
│ ├── assistant.code-workspace
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ └── uv.lock
├── KNOWN_ISSUES.md
├── libraries
│ ├── dotnet
│ │ ├── .editorconfig
│ │ ├── pack.sh
│ │ ├── README.md
│ │ ├── SemanticWorkbench.sln
│ │ ├── SemanticWorkbench.sln.DotSettings
│ │ └── WorkbenchConnector
│ │ ├── AgentBase.cs
│ │ ├── AgentConfig
│ │ │ ├── AgentConfigBase.cs
│ │ │ ├── AgentConfigPropertyAttribute.cs
│ │ │ └── ConfigUtils.cs
│ │ ├── Constants.cs
│ │ ├── IAgentBase.cs
│ │ ├── icon.png
│ │ ├── Models
│ │ │ ├── Command.cs
│ │ │ ├── Conversation.cs
│ │ │ ├── ConversationEvent.cs
│ │ │ ├── DebugInfo.cs
│ │ │ ├── Insight.cs
│ │ │ ├── Message.cs
│ │ │ ├── MessageMetadata.cs
│ │ │ ├── Participant.cs
│ │ │ ├── Sender.cs
│ │ │ └── ServiceInfo.cs
│ │ ├── Storage
│ │ │ ├── AgentInfo.cs
│ │ │ ├── AgentServiceStorage.cs
│ │ │ └── IAgentServiceStorage.cs
│ │ ├── StringLoggingExtensions.cs
│ │ ├── Webservice.cs
│ │ ├── WorkbenchConfig.cs
│ │ ├── WorkbenchConnector.cs
│ │ └── WorkbenchConnector.csproj
│ ├── Makefile
│ └── python
│ ├── anthropic-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── anthropic_client
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── config.py
│ │ │ └── messages.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── assistant-data-gen
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── assistant_data_gen
│ │ │ ├── __init__.py
│ │ │ ├── assistant_api.py
│ │ │ ├── config.py
│ │ │ ├── gce
│ │ │ │ ├── __init__.py
│ │ │ │ ├── gce_agent.py
│ │ │ │ └── prompts.py
│ │ │ └── pydantic_ai_utils.py
│ │ ├── configs
│ │ │ └── document_assistant_example_config.yaml
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── scripts
│ │ │ ├── gce_simulation.py
│ │ │ └── generate_scenario.py
│ │ └── uv.lock
│ ├── assistant-drive
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ └── settings.json
│ │ ├── assistant_drive
│ │ │ ├── __init__.py
│ │ │ ├── drive.py
│ │ │ └── tests
│ │ │ └── test_basic.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── pytest.ini
│ │ ├── README.md
│ │ ├── usage.ipynb
│ │ └── uv.lock
│ ├── assistant-extensions
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── assistant_extensions
│ │ │ ├── __init__.py
│ │ │ ├── ai_clients
│ │ │ │ └── config.py
│ │ │ ├── artifacts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _artifacts.py
│ │ │ │ ├── _inspector.py
│ │ │ │ └── _model.py
│ │ │ ├── attachments
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _attachments.py
│ │ │ │ ├── _convert.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _shared.py
│ │ │ │ └── _summarizer.py
│ │ │ ├── chat_context_toolkit
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _config.py
│ │ │ │ ├── archive
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _archive.py
│ │ │ │ │ └── _summarizer.py
│ │ │ │ ├── message_history
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _history.py
│ │ │ │ │ └── _message.py
│ │ │ │ └── virtual_filesystem
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _archive_file_source.py
│ │ │ │ └── _attachments_file_source.py
│ │ │ ├── dashboard_card
│ │ │ │ ├── __init__.py
│ │ │ │ └── _dashboard_card.py
│ │ │ ├── document_editor
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _extension.py
│ │ │ │ ├── _inspector.py
│ │ │ │ └── _model.py
│ │ │ ├── mcp
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _assistant_file_resource_handler.py
│ │ │ │ ├── _client_utils.py
│ │ │ │ ├── _devtunnel.py
│ │ │ │ ├── _model.py
│ │ │ │ ├── _openai_utils.py
│ │ │ │ ├── _sampling_handler.py
│ │ │ │ ├── _tool_utils.py
│ │ │ │ └── _workbench_file_resource_handler.py
│ │ │ ├── navigator
│ │ │ │ ├── __init__.py
│ │ │ │ └── _navigator.py
│ │ │ └── workflows
│ │ │ ├── __init__.py
│ │ │ ├── _model.py
│ │ │ ├── _workflows.py
│ │ │ └── runners
│ │ │ └── _user_proxy.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ └── attachments
│ │ │ └── test_attachments.py
│ │ └── uv.lock
│ ├── chat-context-toolkit
│ │ ├── .claude
│ │ │ └── settings.local.json
│ │ ├── .env.sample
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── assets
│ │ │ ├── archive_v1.png
│ │ │ ├── history_v1.png
│ │ │ └── vfs_v1.png
│ │ ├── chat_context_toolkit
│ │ │ ├── __init__.py
│ │ │ ├── archive
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _archive_reader.py
│ │ │ │ ├── _archive_task_queue.py
│ │ │ │ ├── _state.py
│ │ │ │ ├── _types.py
│ │ │ │ └── summarization
│ │ │ │ ├── __init__.py
│ │ │ │ └── _summarizer.py
│ │ │ ├── history
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _budget.py
│ │ │ │ ├── _decorators.py
│ │ │ │ ├── _history.py
│ │ │ │ ├── _prioritize.py
│ │ │ │ ├── _types.py
│ │ │ │ └── tool_abbreviations
│ │ │ │ ├── __init__.py
│ │ │ │ └── _tool_abbreviations.py
│ │ │ └── virtual_filesystem
│ │ │ ├── __init__.py
│ │ │ ├── _types.py
│ │ │ ├── _virtual_filesystem.py
│ │ │ ├── README.md
│ │ │ └── tools
│ │ │ ├── __init__.py
│ │ │ ├── _ls_tool.py
│ │ │ ├── _tools.py
│ │ │ └── _view_tool.py
│ │ ├── CLAUDE.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ ├── archive
│ │ │ │ └── test_archive_reader.py
│ │ │ ├── history
│ │ │ │ ├── test_abbreviate_messages.py
│ │ │ │ ├── test_history.py
│ │ │ │ ├── test_pair_and_order_tool_messages.py
│ │ │ │ ├── test_prioritize.py
│ │ │ │ └── test_truncate_messages.py
│ │ │ └── virtual_filesystem
│ │ │ ├── test_virtual_filesystem.py
│ │ │ └── tools
│ │ │ ├── test_ls_tool.py
│ │ │ ├── test_tools.py
│ │ │ └── test_view_tool.py
│ │ └── uv.lock
│ ├── content-safety
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── content_safety
│ │ │ ├── __init__.py
│ │ │ ├── evaluators
│ │ │ │ ├── __init__.py
│ │ │ │ ├── azure_content_safety
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── config.py
│ │ │ │ │ └── evaluator.py
│ │ │ │ ├── config.py
│ │ │ │ ├── evaluator.py
│ │ │ │ └── openai_moderations
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ └── evaluator.py
│ │ │ └── README.md
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── events
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── events
│ │ │ ├── __init__.py
│ │ │ └── events.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── guided-conversation
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── guided_conversation
│ │ │ ├── __init__.py
│ │ │ ├── functions
│ │ │ │ ├── __init__.py
│ │ │ │ ├── conversation_plan.py
│ │ │ │ ├── execution.py
│ │ │ │ └── final_update_plan.py
│ │ │ ├── guided_conversation_agent.py
│ │ │ ├── plugins
│ │ │ │ ├── __init__.py
│ │ │ │ ├── agenda.py
│ │ │ │ └── artifact.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── base_model_llm.py
│ │ │ ├── conversation_helpers.py
│ │ │ ├── openai_tool_calling.py
│ │ │ ├── plugin_helpers.py
│ │ │ └── resources.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── llm-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── llm_client
│ │ │ ├── __init__.py
│ │ │ └── model.py
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── Makefile
│ ├── mcp-extensions
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_extensions
│ │ │ ├── __init__.py
│ │ │ ├── _client_session.py
│ │ │ ├── _model.py
│ │ │ ├── _sampling.py
│ │ │ ├── _server_extensions.py
│ │ │ ├── _tool_utils.py
│ │ │ ├── llm
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chat_completion.py
│ │ │ │ ├── helpers.py
│ │ │ │ ├── llm_types.py
│ │ │ │ ├── mcp_chat_completion.py
│ │ │ │ └── openai_chat_completion.py
│ │ │ └── server
│ │ │ ├── __init__.py
│ │ │ └── storage.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_tool_utils.py
│ │ └── uv.lock
│ ├── mcp-tunnel
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_tunnel
│ │ │ ├── __init__.py
│ │ │ ├── _devtunnel.py
│ │ │ ├── _dir.py
│ │ │ └── _main.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── openai-client
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── openai_client
│ │ │ ├── __init__.py
│ │ │ ├── chat_driver
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chat_driver.ipynb
│ │ │ │ ├── chat_driver.py
│ │ │ │ ├── message_history_providers
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── in_memory_message_history_provider.py
│ │ │ │ │ ├── local_message_history_provider.py
│ │ │ │ │ ├── message_history_provider.py
│ │ │ │ │ └── tests
│ │ │ │ │ └── formatted_instructions_test.py
│ │ │ │ └── README.md
│ │ │ ├── client.py
│ │ │ ├── completion.py
│ │ │ ├── config.py
│ │ │ ├── errors.py
│ │ │ ├── logging.py
│ │ │ ├── messages.py
│ │ │ ├── tokens.py
│ │ │ └── tools.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_command_parsing.py
│ │ │ ├── test_formatted_messages.py
│ │ │ ├── test_messages.py
│ │ │ └── test_tokens.py
│ │ └── uv.lock
│ ├── semantic-workbench-api-model
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── semantic_workbench_api_model
│ │ │ ├── __init__.py
│ │ │ ├── assistant_model.py
│ │ │ ├── assistant_service_client.py
│ │ │ ├── workbench_model.py
│ │ │ └── workbench_service_client.py
│ │ └── uv.lock
│ ├── semantic-workbench-assistant
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── semantic_workbench_assistant
│ │ │ ├── __init__.py
│ │ │ ├── assistant_app
│ │ │ │ ├── __init__.py
│ │ │ │ ├── assistant.py
│ │ │ │ ├── config.py
│ │ │ │ ├── content_safety.py
│ │ │ │ ├── context.py
│ │ │ │ ├── error.py
│ │ │ │ ├── export_import.py
│ │ │ │ ├── protocol.py
│ │ │ │ └── service.py
│ │ │ ├── assistant_service.py
│ │ │ ├── auth.py
│ │ │ ├── canonical.py
│ │ │ ├── command.py
│ │ │ ├── config.py
│ │ │ ├── logging_config.py
│ │ │ ├── settings.py
│ │ │ ├── start.py
│ │ │ └── storage.py
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_assistant_app.py
│ │ │ ├── test_canonical.py
│ │ │ ├── test_config.py
│ │ │ └── test_storage.py
│ │ └── uv.lock
│ └── skills
│ ├── .vscode
│ │ └── settings.json
│ ├── Makefile
│ ├── README.md
│ └── skill-library
│ ├── .vscode
│ │ └── settings.json
│ ├── docs
│ │ └── vs-recipe-tool.md
│ ├── Makefile
│ ├── pyproject.toml
│ ├── README.md
│ ├── skill_library
│ │ ├── __init__.py
│ │ ├── chat_driver_helpers.py
│ │ ├── cli
│ │ │ ├── azure_openai.py
│ │ │ ├── conversation_history.py
│ │ │ ├── README.md
│ │ │ ├── run_routine.py
│ │ │ ├── settings.py
│ │ │ └── skill_logger.py
│ │ ├── engine.py
│ │ ├── llm_info.txt
│ │ ├── logging.py
│ │ ├── README.md
│ │ ├── routine_stack.py
│ │ ├── skill.py
│ │ ├── skills
│ │ │ ├── common
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── bing_search.py
│ │ │ │ ├── consolidate.py
│ │ │ │ ├── echo.py
│ │ │ │ ├── gather_context.py
│ │ │ │ ├── get_content_from_url.py
│ │ │ │ ├── gpt_complete.py
│ │ │ │ ├── select_user_intent.py
│ │ │ │ └── summarize.py
│ │ │ ├── eval
│ │ │ │ ├── __init__.py
│ │ │ │ ├── eval_skill.py
│ │ │ │ └── routines
│ │ │ │ └── eval.py
│ │ │ ├── fabric
│ │ │ │ ├── __init__.py
│ │ │ │ ├── fabric_skill.py
│ │ │ │ ├── patterns
│ │ │ │ │ ├── agility_story
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── ai
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_answers
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_candidates
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_cfp_submission
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_claims
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_comments
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_debate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_email_headers
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_incident
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_interviewer_techniques
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_logs
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_malware
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_military_strategy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_mistakes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_paper
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_patent
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_personality
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_presentation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_product_feedback
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_proposition
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose_json
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_prose_pinker
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_risk
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_sales_call
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_spiritual_text
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_tech_impact
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_threat_report
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── analyze_threat_report_cmds
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── analyze_threat_report_trends
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── answer_interview_question
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── ask_secure_by_design_questions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── ask_uncle_duke
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── capture_thinkers_work
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── check_agreement
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── clean_text
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── coding_master
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── compare_and_contrast
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── convert_to_markdown
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_5_sentence_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_academic_paper
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ai_jobs_analysis
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_aphorisms
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_art_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_better_frame
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_coding_project
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_command
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_cyber_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_design_document
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_diy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_formal_email
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_git_diff_commit
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_graph_from_input
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_hormozi_offer
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_idea_compass
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_investigation_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_keynote
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_logo
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_markmap_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_mermaid_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_mermaid_visualization_for_github
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_micro_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_network_threat_landscape
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_newsletter_entry
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_npc
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_pattern
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_prd
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_prediction_block
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_quiz
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_reading_plan
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_recursive_outline
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_report_finding
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_rpg_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_security_update
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_show_intro
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_sigma_rules
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_story_explanation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_stride_threat_model
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_summary
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_tags
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_threat_scenarios
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ttrc_graph
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_ttrc_narrative
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_upgrade_pack
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_user_story
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── create_video_chapters
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── create_visualization
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── dialog_with_socrates
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── enrich_blog_post
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_code
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── explain_docs
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── explain_math
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_project
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── explain_terms
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── export_data_as_csv
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_algorithm_update_recommendations
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_article_wisdom
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_book_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_book_recommendations
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_business_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_controversial_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_core_message
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_ctf_writeup
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_domains
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_extraordinary_claims
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_ideas
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_insights
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_insights_dm
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_instructions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_jokes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_latest_video
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_main_idea
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_most_redeeming_thing
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_patterns
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_poc
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_predictions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_primary_problem
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_primary_solution
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_product_features
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_questions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_recipe
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_recommendations
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_references
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_skills
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_song_meaning
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_sponsors
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_videoid
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── extract_wisdom
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── extract_wisdom-1.0.0
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_agents
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_dm
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── extract_wisdom_nometa
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── find_hidden_message
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── find_logical_fallacies
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── get_wow_per_minute
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── get_youtube_rss
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── humanize
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_distinctions
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_perspectives
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_relationships
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_dsrp_systems
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── identify_job_stories
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── improve_academic_writing
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── improve_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── improve_report_finding
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── improve_writing
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── judge_output
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── label_and_rate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── loaded
│ │ │ │ │ ├── md_callout
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── official_pattern_template
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── pattern_explanations.md
│ │ │ │ │ ├── prepare_7s_strategy
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── provide_guidance
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_ai_response
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_ai_result
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── rate_content
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── rate_value
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── raw_query
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── raycast
│ │ │ │ │ │ ├── capture_thinkers_work
│ │ │ │ │ │ ├── create_story_explanation
│ │ │ │ │ │ ├── extract_primary_problem
│ │ │ │ │ │ ├── extract_wisdom
│ │ │ │ │ │ └── yt
│ │ │ │ │ ├── recommend_artists
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── recommend_pipeline_upgrades
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── recommend_talkpanel_topics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── refine_design_document
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── review_design
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── sanitize_broken_html_to_markdown
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── show_fabric_options_markmap
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── solve_with_cot
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── stringify
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── suggest_pattern
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize
│ │ │ │ │ │ ├── dmiessler
│ │ │ │ │ │ │ └── summarize
│ │ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ │ └── user.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_debate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_git_changes
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_git_diff
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_lecture
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_legislation
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_meeting
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_micro
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_newsletter
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_paper
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_prompt
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── summarize_pull-requests
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── summarize_rpg_session
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_analyze_challenge_handling
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_check_metrics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_create_h3_career
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_create_opening_sentences
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_describe_life_outlook
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_extract_intro_sentences
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_extract_panel_topics
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_blindspots
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_negative_thinking
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_find_neglected_goals
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_give_encouragement
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_red_team_thinking
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_threat_model_plans
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_visualize_mission_goals_projects
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── t_year_in_review
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── to_flashcards
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── transcribe_minutes
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── translate
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── tweet
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_essay
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_hackerone_report
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_latex
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_micro_essay
│ │ │ │ │ │ └── system.md
│ │ │ │ │ ├── write_nuclei_template_rule
│ │ │ │ │ │ ├── system.md
│ │ │ │ │ │ └── user.md
│ │ │ │ │ ├── write_pull-request
│ │ │ │ │ │ └── system.md
│ │ │ │ │ └── write_semgrep_rule
│ │ │ │ │ ├── system.md
│ │ │ │ │ └── user.md
│ │ │ │ └── routines
│ │ │ │ ├── list.py
│ │ │ │ ├── run.py
│ │ │ │ └── show.py
│ │ │ ├── guided_conversation
│ │ │ │ ├── __init__.py
│ │ │ │ ├── agenda.py
│ │ │ │ ├── artifact_helpers.py
│ │ │ │ ├── chat_completions
│ │ │ │ │ ├── fix_agenda_error.py
│ │ │ │ │ ├── fix_artifact_error.py
│ │ │ │ │ ├── generate_agenda.py
│ │ │ │ │ ├── generate_artifact_updates.py
│ │ │ │ │ ├── generate_final_artifact.py
│ │ │ │ │ └── generate_message.py
│ │ │ │ ├── conversation_guides
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── acrostic_poem.py
│ │ │ │ │ ├── er_triage.py
│ │ │ │ │ ├── interview.py
│ │ │ │ │ └── patient_intake.py
│ │ │ │ ├── guide.py
│ │ │ │ ├── guided_conversation_skill.py
│ │ │ │ ├── logging.py
│ │ │ │ ├── message.py
│ │ │ │ ├── resources.py
│ │ │ │ ├── routines
│ │ │ │ │ └── guided_conversation.py
│ │ │ │ └── tests
│ │ │ │ ├── conftest.py
│ │ │ │ ├── test_artifact_helpers.py
│ │ │ │ ├── test_generate_agenda.py
│ │ │ │ ├── test_generate_artifact_updates.py
│ │ │ │ ├── test_generate_final_artifact.py
│ │ │ │ └── test_resource.py
│ │ │ ├── meta
│ │ │ │ ├── __init__.py
│ │ │ │ ├── meta_skill.py
│ │ │ │ ├── README.md
│ │ │ │ └── routines
│ │ │ │ └── generate_routine.py
│ │ │ ├── posix
│ │ │ │ ├── __init__.py
│ │ │ │ ├── posix_skill.py
│ │ │ │ ├── routines
│ │ │ │ │ ├── append_file.py
│ │ │ │ │ ├── cd.py
│ │ │ │ │ ├── ls.py
│ │ │ │ │ ├── make_home_dir.py
│ │ │ │ │ ├── mkdir.py
│ │ │ │ │ ├── mv.py
│ │ │ │ │ ├── pwd.py
│ │ │ │ │ ├── read_file.py
│ │ │ │ │ ├── rm.py
│ │ │ │ │ ├── touch.py
│ │ │ │ │ └── write_file.py
│ │ │ │ └── sandbox_shell.py
│ │ │ ├── README.md
│ │ │ ├── research
│ │ │ │ ├── __init__.py
│ │ │ │ ├── README.md
│ │ │ │ ├── research_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── answer_question_about_content.py
│ │ │ │ ├── evaluate_answer.py
│ │ │ │ ├── generate_research_plan.py
│ │ │ │ ├── generate_search_query.py
│ │ │ │ ├── update_research_plan.py
│ │ │ │ ├── web_research.py
│ │ │ │ └── web_search.py
│ │ │ ├── research2
│ │ │ │ ├── __init__.py
│ │ │ │ ├── README.md
│ │ │ │ ├── research_skill.py
│ │ │ │ └── routines
│ │ │ │ ├── facts.py
│ │ │ │ ├── make_final_report.py
│ │ │ │ ├── research.py
│ │ │ │ ├── search_plan.py
│ │ │ │ ├── search.py
│ │ │ │ └── visit_pages.py
│ │ │ └── web_research
│ │ │ ├── __init__.py
│ │ │ ├── README.md
│ │ │ ├── research_skill.py
│ │ │ └── routines
│ │ │ ├── facts.py
│ │ │ ├── make_final_report.py
│ │ │ ├── research.py
│ │ │ ├── search_plan.py
│ │ │ ├── search.py
│ │ │ └── visit_pages.py
│ │ ├── tests
│ │ │ ├── test_common_skill.py
│ │ │ ├── test_integration.py
│ │ │ ├── test_routine_stack.py
│ │ │ ├── tst_skill
│ │ │ │ ├── __init__.py
│ │ │ │ └── routines
│ │ │ │ ├── __init__.py
│ │ │ │ └── a_routine.py
│ │ │ └── utilities
│ │ │ ├── test_find_template_vars.py
│ │ │ ├── test_make_arg_set.py
│ │ │ ├── test_paramspec.py
│ │ │ ├── test_parse_command_string.py
│ │ │ └── test_to_string.py
│ │ ├── types.py
│ │ ├── usage.py
│ │ └── utilities.py
│ └── uv.lock
├── LICENSE
├── Makefile
├── mcp-servers
│ ├── ai-assist-content
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── mcp-example-brave-search.md
│ │ ├── mcp-fastmcp-typescript-README.md
│ │ ├── mcp-llms-full.txt
│ │ ├── mcp-metadata-tips.md
│ │ ├── mcp-python-sdk-README.md
│ │ ├── mcp-typescript-sdk-README.md
│ │ ├── pydanticai-documentation.md
│ │ ├── pydanticai-example-question-graph.md
│ │ ├── pydanticai-example-weather.md
│ │ ├── pydanticai-tutorial.md
│ │ └── README.md
│ ├── Makefile
│ ├── mcp-server-bing-search
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_bing_search
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── clean_website.py
│ │ │ │ └── filter_links.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── tools.py
│ │ │ ├── types.py
│ │ │ ├── utils.py
│ │ │ └── web
│ │ │ ├── __init__.py
│ │ │ ├── get_content.py
│ │ │ ├── llm_processing.py
│ │ │ ├── process_website.py
│ │ │ └── search_bing.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_tools.py
│ │ └── uv.lock
│ ├── mcp-server-bundle
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_bundle
│ │ │ ├── __init__.py
│ │ │ └── main.py
│ │ ├── pyinstaller.spec
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-filesystem
│ │ ├── .env.example
│ │ ├── .github
│ │ │ └── workflows
│ │ │ └── ci.yml
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_filesystem
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_filesystem.py
│ │ └── uv.lock
│ ├── mcp-server-filesystem-edit
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── data
│ │ │ ├── attachments
│ │ │ │ ├── Daily Game Ideas.txt
│ │ │ │ ├── Frontend Framework Proposal.txt
│ │ │ │ ├── ReDoodle.txt
│ │ │ │ └── Research Template.tex
│ │ │ ├── test_cases.yaml
│ │ │ └── transcripts
│ │ │ ├── transcript_research_simple.md
│ │ │ ├── transcript_Startup_Idea_1_202503031513.md
│ │ │ ├── transcript_Startup_Idea_2_202503031659.md
│ │ │ └── transcript_Web_Frontends_202502281551.md
│ │ ├── Makefile
│ │ ├── mcp_server_filesystem_edit
│ │ │ ├── __init__.py
│ │ │ ├── app_handling
│ │ │ │ ├── __init__.py
│ │ │ │ ├── excel.py
│ │ │ │ ├── miktex.py
│ │ │ │ ├── office_common.py
│ │ │ │ ├── powerpoint.py
│ │ │ │ └── word.py
│ │ │ ├── config.py
│ │ │ ├── evals
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common.py
│ │ │ │ ├── run_comments.py
│ │ │ │ ├── run_edit.py
│ │ │ │ └── run_ppt_edit.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── add_comments.py
│ │ │ │ ├── analyze_comments.py
│ │ │ │ ├── latex_edit.py
│ │ │ │ ├── markdown_draft.py
│ │ │ │ ├── markdown_edit.py
│ │ │ │ └── powerpoint_edit.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── add_comments.py
│ │ │ │ ├── edit_adapters
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── common.py
│ │ │ │ │ ├── latex.py
│ │ │ │ │ └── markdown.py
│ │ │ │ ├── edit.py
│ │ │ │ └── helpers.py
│ │ │ └── types.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── app_handling
│ │ │ │ ├── test_excel.py
│ │ │ │ ├── test_miktext.py
│ │ │ │ ├── test_office_common.py
│ │ │ │ ├── test_powerpoint.py
│ │ │ │ └── test_word.py
│ │ │ ├── conftest.py
│ │ │ └── tools
│ │ │ └── edit_adapters
│ │ │ ├── test_latex.py
│ │ │ └── test_markdown.py
│ │ └── uv.lock
│ ├── mcp-server-fusion
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── AddInIcon.svg
│ │ ├── config.py
│ │ ├── FusionMCPServerAddIn.manifest
│ │ ├── FusionMCPServerAddIn.py
│ │ ├── mcp_server_fusion
│ │ │ ├── __init__.py
│ │ │ ├── fusion_mcp_server.py
│ │ │ ├── fusion_utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── event_utils.py
│ │ │ │ ├── general_utils.py
│ │ │ │ └── tool_utils.py
│ │ │ ├── mcp_tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── fusion_3d_operation.py
│ │ │ │ ├── fusion_geometry.py
│ │ │ │ ├── fusion_pattern.py
│ │ │ │ └── fusion_sketch.py
│ │ │ └── vendor
│ │ │ └── README.md
│ │ ├── README.md
│ │ └── requirements.txt
│ ├── mcp-server-giphy
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── giphy_search.py
│ │ │ ├── sampling.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ └── utils.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-memory-user-bio
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_memory_user_bio
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-memory-whiteboard
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_memory_whiteboard
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-office
│ │ ├── .env.example
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── build.sh
│ │ ├── data
│ │ │ ├── attachments
│ │ │ │ ├── Daily Game Ideas.txt
│ │ │ │ ├── Frontend Framework Proposal.txt
│ │ │ │ └── ReDoodle.txt
│ │ │ └── word
│ │ │ ├── test_cases.yaml
│ │ │ └── transcripts
│ │ │ ├── transcript_Startup_Idea_1_202503031513.md
│ │ │ ├── transcript_Startup_Idea_2_202503031659.md
│ │ │ └── transcript_Web_Frontends_202502281551.md
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── app_interaction
│ │ │ │ ├── __init__.py
│ │ │ │ ├── excel_editor.py
│ │ │ │ ├── powerpoint_editor.py
│ │ │ │ └── word_editor.py
│ │ │ ├── config.py
│ │ │ ├── constants.py
│ │ │ ├── evals
│ │ │ │ ├── __init__.py
│ │ │ │ ├── common.py
│ │ │ │ ├── run_comment_analysis.py
│ │ │ │ ├── run_feedback.py
│ │ │ │ └── run_markdown_edit.py
│ │ │ ├── helpers.py
│ │ │ ├── markdown_edit
│ │ │ │ ├── __init__.py
│ │ │ │ ├── comment_analysis.py
│ │ │ │ ├── feedback_step.py
│ │ │ │ ├── markdown_edit.py
│ │ │ │ └── utils.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── comment_analysis.py
│ │ │ │ ├── feedback.py
│ │ │ │ ├── markdown_draft.py
│ │ │ │ └── markdown_edit.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ └── types.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_word_editor.py
│ │ └── uv.lock
│ ├── mcp-server-open-deep-research
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── libs
│ │ │ │ └── open_deep_research
│ │ │ │ ├── cookies.py
│ │ │ │ ├── mdconvert.py
│ │ │ │ ├── run_agents.py
│ │ │ │ ├── text_inspector_tool.py
│ │ │ │ ├── text_web_browser.py
│ │ │ │ └── visual_qa.py
│ │ │ ├── open_deep_research.py
│ │ │ ├── server.py
│ │ │ └── start.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ └── uv.lock
│ ├── mcp-server-open-deep-research-clone
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json
│ │ │ └── settings.json
│ │ ├── Makefile
│ │ ├── mcp_server_open_deep_research_clone
│ │ │ ├── __init__.py
│ │ │ ├── azure_openai.py
│ │ │ ├── config.py
│ │ │ ├── logging.py
│ │ │ ├── sampling.py
│ │ │ ├── server.py
│ │ │ ├── start.py
│ │ │ ├── utils.py
│ │ │ └── web_research.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── test
│ │ │ └── test_open_deep_research_clone.py
│ │ └── uv.lock
│ ├── mcp-server-template
│ │ ├── .taplo.toml
│ │ ├── .vscode
│ │ │ └── settings.json
│ │ ├── copier.yml
│ │ ├── README.md
│ │ └── template
│ │ └── {{ project_slug }}
│ │ ├── .env.example.jinja
│ │ ├── .gitignore
│ │ ├── .vscode
│ │ │ ├── launch.json.jinja
│ │ │ └── settings.json
│ │ ├── {{ module_name }}
│ │ │ ├── __init__.py
│ │ │ ├── config.py.jinja
│ │ │ ├── server.py.jinja
│ │ │ └── start.py.jinja
│ │ ├── Makefile.jinja
│ │ ├── pyproject.toml.jinja
│ │ └── README.md.jinja
│ ├── mcp-server-vscode
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── .npmrc
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ ├── launch.json
│ │ │ ├── settings.json
│ │ │ └── tasks.json
│ │ ├── .vscode-test.mjs
│ │ ├── .vscodeignore
│ │ ├── ASSISTANT_BOOTSTRAP.md
│ │ ├── eslint.config.mjs
│ │ ├── images
│ │ │ └── icon.png
│ │ ├── LICENSE
│ │ ├── Makefile
│ │ ├── out
│ │ │ ├── extension.d.ts
│ │ │ ├── extension.js
│ │ │ ├── test
│ │ │ │ ├── extension.test.d.ts
│ │ │ │ └── extension.test.js
│ │ │ ├── tools
│ │ │ │ ├── code_checker.d.ts
│ │ │ │ ├── code_checker.js
│ │ │ │ ├── debug_tools.d.ts
│ │ │ │ ├── debug_tools.js
│ │ │ │ ├── focus_editor.d.ts
│ │ │ │ ├── focus_editor.js
│ │ │ │ ├── search_symbol.d.ts
│ │ │ │ └── search_symbol.js
│ │ │ └── utils
│ │ │ ├── port.d.ts
│ │ │ └── port.js
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── prettier.config.cjs
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── extension.d.ts
│ │ │ ├── extension.ts
│ │ │ ├── test
│ │ │ │ ├── extension.test.d.ts
│ │ │ │ └── extension.test.ts
│ │ │ ├── tools
│ │ │ │ ├── code_checker.d.ts
│ │ │ │ ├── code_checker.ts
│ │ │ │ ├── debug_tools.d.ts
│ │ │ │ ├── debug_tools.ts
│ │ │ │ ├── focus_editor.d.ts
│ │ │ │ ├── focus_editor.ts
│ │ │ │ ├── search_symbol.d.ts
│ │ │ │ └── search_symbol.ts
│ │ │ └── utils
│ │ │ ├── port.d.ts
│ │ │ └── port.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.tsbuildinfo
│ │ ├── vsc-extension-quickstart.md
│ │ └── webpack.config.js
│ └── mcp-server-web-research
│ ├── .env.example
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── Makefile
│ ├── mcp_server_web_research
│ │ ├── __init__.py
│ │ ├── azure_openai.py
│ │ ├── config.py
│ │ ├── logging.py
│ │ ├── sampling.py
│ │ ├── server.py
│ │ ├── start.py
│ │ ├── utils.py
│ │ └── web_research.py
│ ├── pyproject.toml
│ ├── README.md
│ ├── test
│ │ └── test_web_research.py
│ └── uv.lock
├── README.md
├── RESPONSIBLE_AI_FAQ.md
├── ruff.toml
├── SECURITY.md
├── semantic-workbench.code-workspace
├── SUPPORT.md
├── tools
│ ├── build_ai_context_files.py
│ ├── collect_files.py
│ ├── docker
│ │ ├── azure_website_sshd.conf
│ │ ├── docker-entrypoint.sh
│ │ ├── Dockerfile.assistant
│ │ └── Dockerfile.mcp-server
│ ├── makefiles
│ │ ├── docker-assistant.mk
│ │ ├── docker-mcp-server.mk
│ │ ├── docker.mk
│ │ ├── python.mk
│ │ ├── recursive.mk
│ │ └── shell.mk
│ ├── reset-service-data.ps1
│ ├── reset-service-data.sh
│ ├── run-app.ps1
│ ├── run-app.sh
│ ├── run-canonical-agent.ps1
│ ├── run-canonical-agent.sh
│ ├── run-dotnet-examples-with-aspire.sh
│ ├── run-python-example1.sh
│ ├── run-python-example2.ps1
│ ├── run-python-example2.sh
│ ├── run-service.ps1
│ ├── run-service.sh
│ ├── run-workbench-chatbot.ps1
│ └── run-workbench-chatbot.sh
├── workbench-app
│ ├── .dockerignore
│ ├── .env.example
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── settings.json
│ ├── docker-entrypoint.sh
│ ├── Dockerfile
│ ├── docs
│ │ ├── APP_DEV_GUIDE.md
│ │ ├── MESSAGE_METADATA.md
│ │ ├── MESSAGE_TYPES.md
│ │ ├── README.md
│ │ └── STATE_INSPECTORS.md
│ ├── index.html
│ ├── Makefile
│ ├── nginx.conf
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── prettier.config.cjs
│ ├── public
│ │ └── assets
│ │ ├── background-1-upscaled.jpg
│ │ ├── background-1-upscaled.png
│ │ ├── background-1.jpg
│ │ ├── background-1.png
│ │ ├── background-2.jpg
│ │ ├── background-2.png
│ │ ├── experimental-feature.jpg
│ │ ├── favicon.svg
│ │ ├── workflow-designer-1.jpg
│ │ ├── workflow-designer-outlets.jpg
│ │ ├── workflow-designer-states.jpg
│ │ └── workflow-designer-transitions.jpg
│ ├── README.md
│ ├── run.sh
│ ├── src
│ │ ├── components
│ │ │ ├── App
│ │ │ │ ├── AppFooter.tsx
│ │ │ │ ├── AppHeader.tsx
│ │ │ │ ├── AppMenu.tsx
│ │ │ │ ├── AppView.tsx
│ │ │ │ ├── CodeLabel.tsx
│ │ │ │ ├── CommandButton.tsx
│ │ │ │ ├── ConfirmLeave.tsx
│ │ │ │ ├── ContentExport.tsx
│ │ │ │ ├── ContentImport.tsx
│ │ │ │ ├── CopyButton.tsx
│ │ │ │ ├── DialogControl.tsx
│ │ │ │ ├── DynamicIframe.tsx
│ │ │ │ ├── ErrorListFromAppState.tsx
│ │ │ │ ├── ErrorMessageBar.tsx
│ │ │ │ ├── ExperimentalNotice.tsx
│ │ │ │ ├── FormWidgets
│ │ │ │ │ ├── BaseModelEditorWidget.tsx
│ │ │ │ │ ├── CustomizedArrayFieldTemplate.tsx
│ │ │ │ │ ├── CustomizedFieldTemplate.tsx
│ │ │ │ │ ├── CustomizedObjectFieldTemplate.tsx
│ │ │ │ │ └── InspectableWidget.tsx
│ │ │ │ ├── LabelWithDescription.tsx
│ │ │ │ ├── Loading.tsx
│ │ │ │ ├── MenuItemControl.tsx
│ │ │ │ ├── MiniControl.tsx
│ │ │ │ ├── MyAssistantServiceRegistrations.tsx
│ │ │ │ ├── MyItemsManager.tsx
│ │ │ │ ├── OverflowMenu.tsx
│ │ │ │ ├── PresenceMotionList.tsx
│ │ │ │ ├── ProfileSettings.tsx
│ │ │ │ └── TooltipWrapper.tsx
│ │ │ ├── Assistants
│ │ │ │ ├── ApplyConfigButton.tsx
│ │ │ │ ├── AssistantAdd.tsx
│ │ │ │ ├── AssistantConfigExportButton.tsx
│ │ │ │ ├── AssistantConfigImportButton.tsx
│ │ │ │ ├── AssistantConfiguration.tsx
│ │ │ │ ├── AssistantConfigure.tsx
│ │ │ │ ├── AssistantCreate.tsx
│ │ │ │ ├── AssistantDelete.tsx
│ │ │ │ ├── AssistantDuplicate.tsx
│ │ │ │ ├── AssistantExport.tsx
│ │ │ │ ├── AssistantImport.tsx
│ │ │ │ ├── AssistantRemove.tsx
│ │ │ │ ├── AssistantRename.tsx
│ │ │ │ ├── AssistantServiceInfo.tsx
│ │ │ │ ├── AssistantServiceMetadata.tsx
│ │ │ │ └── MyAssistants.tsx
│ │ │ ├── AssistantServiceRegistrations
│ │ │ │ ├── AssistantServiceRegistrationApiKey.tsx
│ │ │ │ ├── AssistantServiceRegistrationApiKeyReset.tsx
│ │ │ │ ├── AssistantServiceRegistrationCreate.tsx
│ │ │ │ └── AssistantServiceRegistrationRemove.tsx
│ │ │ ├── Conversations
│ │ │ │ ├── Canvas
│ │ │ │ │ ├── AssistantCanvas.tsx
│ │ │ │ │ ├── AssistantCanvasList.tsx
│ │ │ │ │ ├── AssistantInspector.tsx
│ │ │ │ │ ├── AssistantInspectorList.tsx
│ │ │ │ │ └── ConversationCanvas.tsx
│ │ │ │ ├── ChatInputPlugins
│ │ │ │ │ ├── ClearEditorPlugin.tsx
│ │ │ │ │ ├── LexicalMenu.ts
│ │ │ │ │ ├── ParticipantMentionsPlugin.tsx
│ │ │ │ │ ├── TypeaheadMenuPlugin.css
│ │ │ │ │ └── TypeaheadMenuPlugin.tsx
│ │ │ │ ├── ContentRenderers
│ │ │ │ │ ├── CodeContentRenderer.tsx
│ │ │ │ │ ├── ContentListRenderer.tsx
│ │ │ │ │ ├── ContentRenderer.tsx
│ │ │ │ │ ├── DiffRenderer.tsx
│ │ │ │ │ ├── HtmlContentRenderer.tsx
│ │ │ │ │ ├── JsonSchemaContentRenderer.tsx
│ │ │ │ │ ├── MarkdownContentRenderer.tsx
│ │ │ │ │ ├── MarkdownEditorRenderer.tsx
│ │ │ │ │ ├── MermaidContentRenderer.tsx
│ │ │ │ │ ├── MusicABCContentRenderer.css
│ │ │ │ │ └── MusicABCContentRenderer.tsx
│ │ │ │ ├── ContextWindow.tsx
│ │ │ │ ├── ConversationCreate.tsx
│ │ │ │ ├── ConversationDuplicate.tsx
│ │ │ │ ├── ConversationExport.tsx
│ │ │ │ ├── ConversationFileIcon.tsx
│ │ │ │ ├── ConversationRemove.tsx
│ │ │ │ ├── ConversationRename.tsx
│ │ │ │ ├── ConversationShare.tsx
│ │ │ │ ├── ConversationShareCreate.tsx
│ │ │ │ ├── ConversationShareList.tsx
│ │ │ │ ├── ConversationShareView.tsx
│ │ │ │ ├── ConversationsImport.tsx
│ │ │ │ ├── ConversationTranscript.tsx
│ │ │ │ ├── DebugInspector.tsx
│ │ │ │ ├── FileItem.tsx
│ │ │ │ ├── FileList.tsx
│ │ │ │ ├── InputAttachmentList.tsx
│ │ │ │ ├── InputOptionsControl.tsx
│ │ │ │ ├── InteractHistory.tsx
│ │ │ │ ├── InteractInput.tsx
│ │ │ │ ├── Message
│ │ │ │ │ ├── AttachmentSection.tsx
│ │ │ │ │ ├── ContentRenderer.tsx
│ │ │ │ │ ├── ContentSafetyNotice.tsx
│ │ │ │ │ ├── InteractMessage.tsx
│ │ │ │ │ ├── MessageActions.tsx
│ │ │ │ │ ├── MessageBase.tsx
│ │ │ │ │ ├── MessageBody.tsx
│ │ │ │ │ ├── MessageContent.tsx
│ │ │ │ │ ├── MessageFooter.tsx
│ │ │ │ │ ├── MessageHeader.tsx
│ │ │ │ │ ├── NotificationAccordion.tsx
│ │ │ │ │ └── ToolResultMessage.tsx
│ │ │ │ ├── MessageDelete.tsx
│ │ │ │ ├── MessageLink.tsx
│ │ │ │ ├── MyConversations.tsx
│ │ │ │ ├── MyShares.tsx
│ │ │ │ ├── ParticipantAvatar.tsx
│ │ │ │ ├── ParticipantAvatarGroup.tsx
│ │ │ │ ├── ParticipantItem.tsx
│ │ │ │ ├── ParticipantList.tsx
│ │ │ │ ├── ParticipantStatus.tsx
│ │ │ │ ├── RewindConversation.tsx
│ │ │ │ ├── ShareRemove.tsx
│ │ │ │ ├── SpeechButton.tsx
│ │ │ │ └── ToolCalls.tsx
│ │ │ └── FrontDoor
│ │ │ ├── Chat
│ │ │ │ ├── AssistantDrawer.tsx
│ │ │ │ ├── CanvasDrawer.tsx
│ │ │ │ ├── Chat.tsx
│ │ │ │ ├── ChatCanvas.tsx
│ │ │ │ ├── ChatControls.tsx
│ │ │ │ └── ConversationDrawer.tsx
│ │ │ ├── Controls
│ │ │ │ ├── AssistantCard.tsx
│ │ │ │ ├── AssistantSelector.tsx
│ │ │ │ ├── AssistantServiceSelector.tsx
│ │ │ │ ├── ConversationItem.tsx
│ │ │ │ ├── ConversationList.tsx
│ │ │ │ ├── ConversationListOptions.tsx
│ │ │ │ ├── NewConversationButton.tsx
│ │ │ │ ├── NewConversationForm.tsx
│ │ │ │ └── SiteMenuButton.tsx
│ │ │ ├── GlobalContent.tsx
│ │ │ └── MainContent.tsx
│ │ ├── Constants.ts
│ │ ├── global.d.ts
│ │ ├── index.css
│ │ ├── libs
│ │ │ ├── AppStorage.ts
│ │ │ ├── AuthHelper.ts
│ │ │ ├── EventSubscriptionManager.ts
│ │ │ ├── Theme.ts
│ │ │ ├── useAssistantCapabilities.ts
│ │ │ ├── useChatCanvasController.ts
│ │ │ ├── useConversationEvents.ts
│ │ │ ├── useConversationUtility.ts
│ │ │ ├── useCreateConversation.ts
│ │ │ ├── useDebugComponentLifecycle.ts
│ │ │ ├── useDragAndDrop.ts
│ │ │ ├── useEnvironment.ts
│ │ │ ├── useExportUtility.ts
│ │ │ ├── useHistoryUtility.ts
│ │ │ ├── useKeySequence.ts
│ │ │ ├── useMediaQuery.ts
│ │ │ ├── useMicrosoftGraph.ts
│ │ │ ├── useNotify.tsx
│ │ │ ├── useParticipantUtility.tsx
│ │ │ ├── useSiteUtility.ts
│ │ │ ├── useWorkbenchEventSource.ts
│ │ │ ├── useWorkbenchService.ts
│ │ │ └── Utility.ts
│ │ ├── main.tsx
│ │ ├── models
│ │ │ ├── Assistant.ts
│ │ │ ├── AssistantCapability.ts
│ │ │ ├── AssistantServiceInfo.ts
│ │ │ ├── AssistantServiceRegistration.ts
│ │ │ ├── Config.ts
│ │ │ ├── Conversation.ts
│ │ │ ├── ConversationFile.ts
│ │ │ ├── ConversationMessage.ts
│ │ │ ├── ConversationMessageDebug.ts
│ │ │ ├── ConversationParticipant.ts
│ │ │ ├── ConversationShare.ts
│ │ │ ├── ConversationShareRedemption.ts
│ │ │ ├── ConversationState.ts
│ │ │ ├── ConversationStateDescription.ts
│ │ │ ├── ServiceEnvironment.ts
│ │ │ └── User.ts
│ │ ├── redux
│ │ │ ├── app
│ │ │ │ ├── hooks.ts
│ │ │ │ ├── rtkQueryErrorLogger.ts
│ │ │ │ └── store.ts
│ │ │ └── features
│ │ │ ├── app
│ │ │ │ ├── appSlice.ts
│ │ │ │ └── AppState.ts
│ │ │ ├── chatCanvas
│ │ │ │ ├── chatCanvasSlice.ts
│ │ │ │ └── ChatCanvasState.ts
│ │ │ ├── localUser
│ │ │ │ ├── localUserSlice.ts
│ │ │ │ └── LocalUserState.ts
│ │ │ └── settings
│ │ │ ├── settingsSlice.ts
│ │ │ └── SettingsState.ts
│ │ ├── Root.tsx
│ │ ├── routes
│ │ │ ├── AcceptTerms.tsx
│ │ │ ├── AssistantEditor.tsx
│ │ │ ├── AssistantServiceRegistrationEditor.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── ErrorPage.tsx
│ │ │ ├── FrontDoor.tsx
│ │ │ ├── Login.tsx
│ │ │ ├── Settings.tsx
│ │ │ ├── ShareRedeem.tsx
│ │ │ └── Shares.tsx
│ │ ├── services
│ │ │ └── workbench
│ │ │ ├── assistant.ts
│ │ │ ├── assistantService.ts
│ │ │ ├── conversation.ts
│ │ │ ├── file.ts
│ │ │ ├── index.ts
│ │ │ ├── participant.ts
│ │ │ ├── share.ts
│ │ │ ├── state.ts
│ │ │ └── workbench.ts
│ │ └── vite-env.d.ts
│ ├── tools
│ │ └── filtered-ts-prune.cjs
│ ├── tsconfig.json
│ └── vite.config.ts
└── workbench-service
├── .env.example
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── alembic.ini
├── devdb
│ ├── docker-compose.yaml
│ └── postgresql-init.sh
├── Dockerfile
├── Makefile
├── migrations
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions
│ ├── 2024_09_19_000000_69dcda481c14_init.py
│ ├── 2024_09_19_190029_dffb1d7e219a_file_version_filename.py
│ ├── 2024_09_20_204130_b29524775484_share.py
│ ├── 2024_10_30_231536_039bec8edc33_index_message_type.py
│ ├── 2024_11_04_204029_5149c7fb5a32_conversationmessagedebug.py
│ ├── 2024_11_05_015124_245baf258e11_double_check_debugs.py
│ ├── 2024_11_25_191056_a106de176394_drop_workflow.py
│ ├── 2025_03_19_140136_aaaf792d4d72_set_user_title_set.py
│ ├── 2025_03_21_153250_3763629295ad_add_assistant_template_id.py
│ ├── 2025_05_19_163613_b2f86e981885_delete_context_transfer_assistants.py
│ └── 2025_06_18_174328_503c739152f3_delete_knowlege_transfer_assistants.py
├── pyproject.toml
├── README.md
├── semantic_workbench_service
│ ├── __init__.py
│ ├── api.py
│ ├── assistant_api_key.py
│ ├── auth.py
│ ├── azure_speech.py
│ ├── config.py
│ ├── controller
│ │ ├── __init__.py
│ │ ├── assistant_service_client_pool.py
│ │ ├── assistant_service_registration.py
│ │ ├── assistant.py
│ │ ├── conversation_share.py
│ │ ├── conversation.py
│ │ ├── convert.py
│ │ ├── exceptions.py
│ │ ├── export_import.py
│ │ ├── file.py
│ │ ├── participant.py
│ │ └── user.py
│ ├── db.py
│ ├── event.py
│ ├── files.py
│ ├── logging_config.py
│ ├── middleware.py
│ ├── query.py
│ ├── service_user_principals.py
│ ├── service.py
│ └── start.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── docker-compose.yaml
│ ├── test_assistant_api_key.py
│ ├── test_files.py
│ ├── test_integration.py
│ ├── test_middleware.py
│ ├── test_migrations.py
│ ├── test_workbench_service.py
│ └── types.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/ai_context/generated/ASSISTANT_PROJECT.md:
--------------------------------------------------------------------------------
```markdown
1 | # assistants/project-assistant
2 |
3 | [collect-files]
4 |
5 | **Search:** ['assistants/project-assistant']
6 | **Exclude:** ['.venv', 'node_modules', '*.lock', '.git', '__pycache__', '*.pyc', '*.ruff_cache', 'logs', 'output', '*.svg', '*.png']
7 | **Include:** ['pyproject.toml', 'README.md', 'CLAUDE.md']
8 | **Date:** 5/29/2025, 11:45:28 AM
9 | **Files:** 67
10 |
11 | === File: CLAUDE.md ===
12 | # Semantic Workbench Developer Guidelines
13 |
14 | ## AI Context System
15 | **Generate comprehensive codebase context for development:**
16 | * `make ai-context-files` - Generate AI context files for all components
17 | * Files created in `ai_context/generated/` organized by logical boundaries:
18 | - **Python Libraries** (by functional group):
19 | - `PYTHON_LIBRARIES_CORE.md` - Core API model, assistant framework, events
20 | - `PYTHON_LIBRARIES_AI_CLIENTS.md` - Anthropic, OpenAI, LLM clients
21 | - `PYTHON_LIBRARIES_EXTENSIONS.md` - Assistant/MCP extensions, content safety
22 | - `PYTHON_LIBRARIES_SPECIALIZED.md` - Guided conversation, assistant drive
23 | - `PYTHON_LIBRARIES_SKILLS.md` - Skills library with patterns and routines
24 | - **Assistants** (by individual implementation):
25 | - `ASSISTANTS_OVERVIEW.md` - Common patterns and all assistant summaries
26 | - `ASSISTANT_PROJECT.md` - Project assistant (most complex)
27 | - `ASSISTANT_DOCUMENT.md` - Document processing assistant
28 | - `ASSISTANT_CODESPACE.md` - Development environment assistant
29 | - `ASSISTANT_NAVIGATOR.md` - Workbench navigation assistant
30 | - `ASSISTANT_PROSPECTOR.md` - Advanced agent with artifact creation
31 | - `ASSISTANTS_OTHER.md` - Explorer, guided conversation, skill assistants
32 | - **Platform Components**:
33 | - `WORKBENCH_FRONTEND.md` - React app components and UI patterns
34 | - `WORKBENCH_SERVICE.md` - Backend API, database, and service logic
35 | - `MCP_SERVERS.md` - Model Context Protocol server implementations
36 | - `DOTNET_LIBRARIES.md` - .NET libraries and connectors
37 | - **Supporting Files**:
38 | - `EXAMPLES.md` - Sample code and getting-started templates
39 | - `TOOLS.md` - Build scripts and development utilities
40 | - `CONFIGURATION.md` - Root-level configs and project setup
41 | - `ASPIRE_ORCHESTRATOR.md` - Container orchestration setup
42 |
43 | **Using AI Context for Development:**
44 | * **New developers**: Read `CONFIGURATION.md` + `PYTHON_LIBRARIES_CORE.md` for project overview
45 | * **Building assistants**:
46 | - Start with `ASSISTANTS_OVERVIEW.md` for common patterns
47 | - Use specific assistant files (e.g., `ASSISTANT_PROJECT.md`) as implementation templates
48 | * **Working on specific assistants**: Load the relevant `ASSISTANT_*.md` file for focused context
49 | * **Library development**: Choose appropriate `PYTHON_LIBRARIES_*.md` file by functional area
50 | * **Frontend work**: Study component patterns in `WORKBENCH_FRONTEND.md`
51 | * **API development**: Follow service patterns from `WORKBENCH_SERVICE.md`
52 | * **MCP servers**: Use existing servers in `MCP_SERVERS.md` as templates
53 | * **AI tools**: Provide relevant context files for better code generation and debugging
54 | * **Code reviews**: Reference context files to understand cross-component impacts
55 |
56 | ## Common Commands
57 | * Build/Install: `make install` (recursive for all subdirectories)
58 | * Format: `make format` (runs ruff formatter)
59 | * Lint: `make lint` (runs ruff linter)
60 | * Type-check: `make type-check` (runs pyright)
61 | * Test: `make test` (runs pytest)
62 | * Single test: `uv run pytest tests/test_file.py::test_function -v`
63 | * Frontend: `cd workbench-app && pnpm dev` (starts dev server)
64 | * Workbench service: `cd workbench-service && python -m semantic_workbench_service.start`
65 |
66 | ## Code Style
67 | ### Python
68 | * Indentation: 4 spaces
69 | * Line length: 120 characters
70 | * Imports: stdlib → third-party → local, alphabetized within groups
71 | * Naming: `snake_case` for functions/variables, `CamelCase` for classes, `UPPER_SNAKE_CASE` for constants
72 | * Types: Use type annotations consistently; prefer Union syntax (`str | None`) for Python 3.10+
73 | * Documentation: Triple-quote docstrings with param/return descriptions
74 |
75 | ### C# (.NET)
76 | * Naming: `PascalCase` for classes/methods/properties, `camelCase` for parameters/local variables, `_camelCase` for private fields
77 | * Error handling: Use try/catch with specific exceptions, `ConfigureAwait(false)` with async
78 | * Documentation: XML comments for public APIs
79 | * Async: Use async/await consistently with cancellation tokens
80 |
81 | ### TypeScript/React (Frontend)
82 | * Component files: Use PascalCase for component names and files (e.g., `MessageHeader.tsx`)
83 | * Hooks: Prefix with 'use' (e.g., `useConversationEvents.ts`)
84 | * CSS: Use Fluent UI styling with mergeStyle and useClasses pattern
85 | * State management: Redux with Redux Toolkit and RTK Query
86 | * Models: Define strong TypeScript interfaces/types
87 |
88 | ## Tools
89 | * Python: Uses uv for environment/dependency management
90 | * Linting/Formatting: Ruff (Python), ESLint (TypeScript)
91 | * Type checking: Pyright (Python), TypeScript compiler
92 | * Testing: pytest (Python), React Testing Library (Frontend)
93 | * Frontend: React, Fluent UI v9, Fluent Copilot components
94 | * Package management: uv (Python), pnpm (Frontend)
95 |
96 | === File: README.md ===
97 | # Semantic Workbench
98 |
99 | Semantic Workbench is a versatile tool designed to help prototype intelligent assistants quickly.
100 | It supports the creation of new assistants or the integration of existing ones, all within a
101 | cohesive interface. The workbench provides a user-friendly UI for creating conversations with one
102 | or more assistants, configuring settings, and exposing various behaviors.
103 |
104 | The Semantic Workbench is composed of three main components:
105 |
106 | - [Workbench Service](workbench-service/README.md) (Python): The backend service that
107 | handles core functionalities.
108 | - [Workbench App](workbench-app/README.md) (React/Typescript): The frontend web user
109 | interface for interacting with workbench and assistants.
110 | - [Assistant Services](examples) (Python, C#, etc.): any number of assistant services that implement the service protocols/APIs,
111 | developed using any framework and programming language of your choice.
112 |
113 | Designed to be agnostic of any agent framework, language, or platform, the Semantic Workbench
114 | facilitates experimentation, development, testing, and measurement of agent behaviors and workflows.
115 | Assistants integrate with the workbench via a RESTful API, allowing for flexibility and broad applicability in various development environments.
116 |
117 | 
118 |
119 | # Workbench interface examples
120 |
121 | 
122 |
123 | 
124 |
125 | 
126 |
127 | 
128 |
129 | 
130 |
131 | # Quick start (Recommended) - GitHub Codespaces for turn-key development environment
132 |
133 | GitHub Codespaces provides a cloud-based development environment for your repository. It allows you to develop, build, and test your code
134 | in a consistent environment, without needing to install dependencies or configure your local machine. It works with any system with a web
135 | browser and internet connection, including Windows, MacOS, Linux, Chromebooks, tablets, and mobile devices.
136 |
137 | See the [GitHub Codespaces / devcontainer README](.devcontainer/README.md) for more information on how to set up and use GitHub Codespaces
138 | with Semantic Workbench.
139 |
140 | ## Local development environment
141 |
142 | See the [setup guide](docs/SETUP_DEV_ENVIRONMENT.md) on how to configure your dev environment. Or if you have Docker installed you can use dev containers with VS Code which will function similarly to Codespaces.
143 |
144 | ## Using VS Code
145 |
146 | Codespaces will is configured to use `semantic-workbench.code-workspace`, if you are working locally that is recommended over opening the repo root. This ensures that all project configurations, such as tools, formatters, and linters, are correctly applied in VS Code. This avoids issues like incorrect error reporting and non-functional tools.
147 |
148 | Workspace files allow us to manage multiple projects within a monorepo more effectively. Each project can use its own virtual environment (venv), maintaining isolation and avoiding dependency conflicts. Multi-root workspaces (\*.code-workspace files) can point to multiple projects, each configured with its own Python interpreter, ensuring seamless functionality of Python tools and extensions.
149 |
150 | ### Start the app and service
151 |
152 | - Use VS Code > `Run and Debug` (Ctrl/Cmd+Shift+D) > `semantic-workbench` to start the project
153 | - Open your browser and navigate to `https://127.0.0.1:4000`
154 | - You may receive a warning about the app not being secure; click `Advanced` and `Proceed to localhost` to continue
155 | - You can now interact with the app and service in the browser
156 |
157 | ### Start an assistant service:
158 |
159 | - Launch an example an [example](examples/) assistant service:
160 | - No llm api keys needed
161 | - Use VS Code > `Run and Debug` (Ctrl/Cmd+Shift+D) > `examples: python-01-echo-bot` to start the example assistant that echos your messages. This is a good base to understand the basics of building your own assistant.
162 | - Bring your own llm api keys
163 | - Use VS Code > `Run and Debug` (Ctrl/Cmd+Shift+D) > `examples: python-02-simple-chatbot` to start the example chatbot assistant. Either set your keys in your .env file or after creating the assistant as described below, select it and provide the keys in the configuration page.
164 |
165 | ## Open the Workbench and create an Assistant
166 |
167 | Open the app in your browser at [`https://localhost:4000`](https://localhost:4000). When you first log into the Semantic Workbench, follow these steps to get started:
168 |
169 | 1. **Create an Assistant**: On the dashboard, click the `New Assistant` button. Select a template from the available assistant services, provide a name, and click `Save`.
170 |
171 | 2. **Start a Conversation**: On the dashboard, click the `New Conversation` button. Provide a title for the conversation and click `Save`.
172 |
173 | 3. **Add the Assistant**: In the conversation window, click the conversation canvas icon and add your assistant to the conversation from the conversation canvas. Now you can converse with your assistant using the message box at the bottom of the conversation window.
174 |
175 | 
176 |
177 | 
178 |
179 | Expected: You get a response from your assistant!
180 |
181 | Note that the workbench provides capabilities that not all examples use, for example providing attachments. See the [Semantic Workbench](docs/WORKBENCH_APP.md) for more details.
182 |
183 | # Developing your own assistants
184 |
185 | To develop new assistants and connect existing ones, see the [Assistant Development Guide](docs/ASSISTANT_DEVELOPMENT_GUIDE.md) or any check out one of the [examples](examples).
186 |
187 | - [Python example 1](examples/python/python-01-echo-bot/README.md): a simple assistant echoing text back.
188 | - [Python example 2](examples/python/python-02-simple-chatbot/README.md): a simple chatbot implementing metaprompt guardrails and content moderation.
189 | - [Python example 3](examples/python/python-03-multimodel-chatbot/README.md): an extension of the simple chatbot that supports configuration against additional llms.
190 | - [.NET example 1](examples/dotnet/dotnet-01-echo-bot/README.md): a simple agent with echo and support for a basic `/say` command.
191 | - [.NET example 2](examples/dotnet/dotnet-02-message-types-demo/README.md): a simple assistants showcasing Azure AI Content Safety integration and some workbench features like Mermaid graphs.
192 | - [.NET example 3](examples/dotnet/dotnet-03-simple-chatbot/README.md): a functional chatbot implementing metaprompt guardrails and content moderation.
193 |
194 | ## Starting the workbench from the command line
195 |
196 | - Run the script `tools\run-workbench-chatbot.sh` or `tools\run-workbench-chatbot.ps` which does the following:
197 | - Starts the backend service, see [here for instructions](workbench-service/README.md).
198 | - Starts the frontend app, see [here for instructions](workbench-app/README.md).
199 | - Starts the [Python chatbot example](examples/python/python-02-simple-chatbot/README.md)
200 |
201 | ## Refreshing Dev Environment
202 |
203 | - Use the `tools\reset-service-data.sh` or `tools\reset-service-data.sh` script to reset all service data. You can also delete `~/workbench-service/.data` or specific files if you know which one(s).
204 | - From repo root, run `make clean install`.
205 | - This will perform a `git clean` and run installs in all sub-directories
206 | - Or a faster option if you just want to install semantic workbench related stuff:
207 | - From repo root, run `make clean`
208 | - From `~/workbench-app`, run `make install`
209 | - From `~/workbench-service`, run `make install`
210 |
211 | # Contributing
212 |
213 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
214 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
215 | the rights to use your contribution. For details, visit <https://cla.opensource.microsoft.com>.
216 |
217 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
218 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
219 | provided by the bot. You will only need to do this once across all repos using our CLA.
220 |
221 | Please see the detailed [contributing guide](CONTRIBUTING.md) for more information on how you can get involved.
222 |
223 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
224 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
225 | contact [[email protected]](mailto:[email protected]) with any additional questions or comments.
226 |
227 | # Trademarks
228 |
229 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
230 | trademarks or logos is subject to and must follow
231 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
232 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
233 | Any use of third-party trademarks or logos are subject to those third-party's policies.
234 |
235 |
236 | === File: assistants/project-assistant/.env.example ===
237 | # Description: Example of .env file
238 | # Usage: Copy this file to .env and set the values
239 |
240 | # NOTE:
241 | # - Environment variables in the host environment will take precedence over values in this file.
242 | # - When running with VS Code, you must 'stop' and 'start' the process for changes to take effect.
243 | # It is not enough to just use the VS Code 'restart' button
244 |
245 | # Assistant Service
246 | ASSISTANT__AZURE_OPENAI_ENDPOINT=https://<YOUR-RESOURCE-NAME>.openai.azure.com/
247 | ASSISTANT__AZURE_CONTENT_SAFETY_ENDPOINT=https://<YOUR-RESOURCE-NAME>.cognitiveservices.azure.com/
248 |
249 |
250 | === File: assistants/project-assistant/.vscode/launch.json ===
251 | {
252 | "version": "0.2.0",
253 | "configurations": [
254 | {
255 | "type": "debugpy",
256 | "request": "launch",
257 | "name": "assistants: project-assistant",
258 | "cwd": "${workspaceFolder}",
259 | "module": "semantic_workbench_assistant.start",
260 | "consoleTitle": "${workspaceFolderBasename}",
261 | "justMyCode": false // Set to false to debug external libraries
262 | }
263 | ]
264 | }
265 |
266 |
267 | === File: assistants/project-assistant/.vscode/settings.json ===
268 | {
269 | "editor.bracketPairColorization.enabled": true,
270 | "editor.codeActionsOnSave": {
271 | "source.organizeImports": "explicit",
272 | "source.fixAll": "explicit"
273 | },
274 | "editor.guides.bracketPairs": "active",
275 | "editor.formatOnPaste": true,
276 | "editor.formatOnType": true,
277 | "editor.formatOnSave": true,
278 | "files.eol": "\n",
279 | "files.exclude": {
280 | "**/.git": true,
281 | "**/.svn": true,
282 | "**/.hg": true,
283 | "**/CVS": true,
284 | "**/.DS_Store": true,
285 | "**/Thumbs.db": true
286 | },
287 | "files.trimTrailingWhitespace": true,
288 | "[json]": {
289 | "editor.defaultFormatter": "esbenp.prettier-vscode",
290 | "editor.formatOnSave": true
291 | },
292 | "[jsonc]": {
293 | "editor.defaultFormatter": "esbenp.prettier-vscode",
294 | "editor.formatOnSave": true
295 | },
296 | "python.analysis.autoFormatStrings": true,
297 | "python.analysis.autoImportCompletions": true,
298 | "python.analysis.diagnosticMode": "workspace",
299 | "python.analysis.fixAll": ["source.unusedImports"],
300 | "python.analysis.inlayHints.functionReturnTypes": true,
301 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
302 | "[python]": {
303 | "editor.defaultFormatter": "charliermarsh.ruff",
304 | "editor.formatOnSave": true,
305 | "editor.codeActionsOnSave": {
306 | "source.fixAll": "explicit",
307 | "source.unusedImports": "explicit",
308 | "source.organizeImports": "explicit",
309 | "source.formatDocument": "explicit"
310 | }
311 | },
312 | "ruff.nativeServer": "on",
313 | "search.exclude": {
314 | "**/.venv": true,
315 | "**/.data": true,
316 | "**/__pycache__": true
317 | },
318 |
319 | // For use with optional extension: "streetsidesoftware.code-spell-checker"
320 | "cSpell.ignorePaths": [
321 | ".venv",
322 | "node_modules",
323 | "package-lock.json",
324 | "settings.json",
325 | "uv.lock"
326 | ],
327 | "cSpell.words": [
328 | "Codespaces",
329 | "contentsafety",
330 | "debugpy",
331 | "deepmerge",
332 | "devcontainer",
333 | "dotenv",
334 | "endregion",
335 | "Excalidraw",
336 | "fastapi",
337 | "GIPHY",
338 | "jsonschema",
339 | "Langchain",
340 | "modelcontextprotocol",
341 | "moderations",
342 | "mzxrai",
343 | "openai",
344 | "pdfplumber",
345 | "pydantic",
346 | "pyproject",
347 | "pyright",
348 | "pytest",
349 | "semanticworkbench",
350 | "semanticworkbenchteam",
351 | "tiktoken",
352 | "updown",
353 | "virtualenvs",
354 | "webresearch"
355 | ]
356 | }
357 |
358 |
359 | === File: assistants/project-assistant/CLAUDE.md ===
360 | # CLAUDE.md
361 |
362 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
363 |
364 | # Semantic Workbench Developer Guidelines
365 |
366 | ## Common Commands
367 | * Build/Install: `make install` (recursive for all subdirectories)
368 | * Format: `make format` (runs ruff formatter)
369 | * Lint: `make lint` (runs ruff linter)
370 | * Type-check: `make type-check` (runs pyright)
371 | * Test: `make test` (runs pytest)
372 | * Single test: `uv run pytest tests/test_file.py::test_function -v`
373 |
374 | ## Code Style
375 | ### Python
376 | * Indentation: 4 spaces
377 | * Line length: 120 characters
378 | * Imports: stdlib → third-party → local, alphabetized within groups
379 | * Naming: `snake_case` for functions/variables, `CamelCase` for classes, `UPPER_SNAKE_CASE` for constants
380 | * Types: Use type annotations consistently; prefer Union syntax (`str | None`) for Python 3.10+
381 | * Documentation: Triple-quote docstrings with param/return descriptions
382 |
383 | ## Tools
384 | * Python: Uses uv for environment/dependency management
385 | * Linting/Formatting: Ruff (Python)
386 | * Type checking: Pyright (Python)
387 | * Testing: pytest (Python)
388 | * Package management: uv (Python)Ok.
389 |
390 | === File: assistants/project-assistant/Makefile ===
391 | repo_root = $(shell git rev-parse --show-toplevel)
392 | include $(repo_root)/tools/makefiles/python.mk
393 | include $(repo_root)/tools/makefiles/docker-assistant.mk
394 |
395 |
396 | === File: assistants/project-assistant/README.md ===
397 | # Project Assistant
398 |
399 | A dual-mode context transfer system that facilitates collaborative projects between Coordinators and Team members in the Semantic Workbench.
400 |
401 | ## Overview
402 |
403 | The Project Assistant is designed to bridge the information gap between project Coordinators and Team members by providing a structured communication system with shared artifacts, real-time updates, and bidirectional information flow. It enables:
404 |
405 | - **Project Definition**: Coordinators can create detailed project briefs with goals and success criteria
406 | - **Information Sharing**: Knowledge transfer between separate conversations
407 | - **Information Requests**: Team members can request information or assistance from Coordinators
408 | - **Progress Tracking**: Real-time project dashboard updates and completion criteria
409 | - **Inspector Panel**: Visual dashboard showing project state and progress
410 |
411 | ## Key Features
412 |
413 | ### Conversation Types and Dual Mode Operation
414 |
415 | The Project Assistant creates and manages three distinct types of conversations:
416 |
417 | 1. **Coordinator Conversation**: The personal conversation used by the project coordinator/owner to create and manage the project.
418 |
419 | 2. **Shareable Team Conversation**: A template conversation that's automatically created along with a share URL. This conversation is never directly used - it serves as the template for creating individual team conversations when users click the share link.
420 |
421 | 3. **Team Conversation(s)**: Individual conversations for team members, created when they redeem the share URL. Each team member gets their own personal conversation connected to the project.
422 |
423 | The assistant operates in two distinct modes with different capabilities:
424 |
425 | 1. **Coordinator Mode (Planning Stage)**
426 | - Create project briefs with clear goals and success criteria
427 | - Maintain an auto-updating project whiteboard with critical information
428 | - Provide guidance and respond to information requests
429 | - Control the "Ready for Working" milestone when project definition is complete
430 |
431 | 2. **Team Mode (Working Stage)**
432 | - Access project brief and project whiteboard
433 | - Mark success criteria as completed
434 | - Log requests for information or assistance from Coordinators
435 | - Update project dashboard with progress information
436 | - Report project completion when all criteria are met
437 |
438 | ### Key Artifacts
439 |
440 | The system manages several core artifacts that support project operations:
441 |
442 | - **Project Brief**: Details project goals and success criteria
443 | - **Project Whiteboard**: Dynamically updated information repository that captures key project context
444 | - **Information Requests**: Documented information needs from Team members
445 | - **Project Dashboard**: Real-time progress tracking and state information
446 |
447 | ### State Management
448 |
449 | The assistant uses a multi-layered state management approach:
450 |
451 | - **Cross-Conversation Linking**: Connects Coordinator and Team conversations
452 | - **File Synchronization**: Automatic file sharing between conversations, including when files are uploaded by Coordinators or when team members return to a conversation
453 | - **Inspector Panel**: Real-time visual status dashboard for project progress
454 | - **Conversation-Specific Storage**: Each conversation maintains role-specific state
455 |
456 | ## Usage
457 |
458 | ### Commands
459 |
460 | #### Common Commands
461 | - `/status` - View current project status and progress
462 | - `/info [brief|whiteboard|requests|all]` - View project information
463 |
464 | #### Coordinator Commands
465 | - `/create-project <name> | <description>` - Create a new project
466 | - `/add-goal <name> | <description> | [criteria1;criteria2;...]` - Add a project goal
467 | - `/add-kb-section <title> | <content>` - Add whiteboard content manually
468 | - `/ready-for-working` - Mark project as ready for team operations
469 | - `/invite` - Generate project invitation for team members
470 | - `/resolve <request-id> | <resolution>` - Resolve an information request
471 |
472 | #### Team Commands
473 | - `/join <invitation-code>` - Join an existing project
474 | - `/request-info <title> | <description> | [priority]` - Create information request
475 | - `/update-status <status> | <progress> | <message>` - Update project status
476 | - `/complete-criteria <goal-index> <criteria-index>` - Mark criterion as complete
477 | - `/complete-project` - Report project completion
478 |
479 | ### Workflow
480 |
481 | 1. **Coordinator Preparation**:
482 | - Create project brief with goals and success criteria
483 | - The project whiteboard automatically updates with key information
484 | - Generate invitation link for team members
485 | - Mark project as ready for working
486 |
487 | 2. **Team Operations**:
488 | - Join project using invitation link
489 | - Review project brief and whiteboard content
490 | - Execute project tasks and track progress
491 | - Create information requests when information is needed
492 | - Mark criteria as completed when achieved
493 | - Report project completion when all goals are met
494 |
495 | 3. **Collaborative Cycle**:
496 | - Coordinator responds to information requests
497 | - Team updates project status with progress
498 | - Both sides can view project status and progress via inspector panel
499 |
500 | ## Development
501 |
502 | ### Project Structure
503 |
504 | - `/assistant/`: Core implementation files
505 | - `chat.py`: Main assistant implementation with event handlers
506 | - `project_tools.py`: Tool functions for the LLM to use
507 | - `state_inspector.py`: Inspector panel implementation
508 | - `project_manager.py`: Project state and artifact management
509 | - `artifact_messaging.py`: Cross-conversation artifact sharing
510 | - `command_processor.py`: Command handling logic
511 |
512 | - `/docs/`: Documentation files
513 | - `DESIGN.md`: System design and architecture
514 | - `DEV_GUIDE.md`: Development guidelines
515 | - `ASSISTANT_LIBRARY_NOTES.md`: Notes on the assistant library
516 | - `WORKBENCH_NOTES.md`: Workbench state management details
517 |
518 | - `/tests/`: Test files covering key functionality
519 |
520 | ### Development Commands
521 |
522 | ```bash
523 | # Install dependencies
524 | make install
525 |
526 | # Run tests
527 | make test
528 |
529 | # Type checking
530 | make type-check
531 |
532 | # Linting
533 | make lint
534 | ```
535 |
536 | ## Architecture
537 |
538 | The Project Assistant leverages the Semantic Workbench Assistant library for core functionality and extends it with:
539 |
540 | 1. **Cross-Conversation Communication**: Using the conversation sharing API
541 | 2. **Artifact Management**: Structured data models for project information
542 | 3. **State Inspection**: Real-time project status dashboard
543 | 4. **Tool-based Interaction**: LLM functions for project tasks
544 | 5. **Role-Specific Experiences**: Tailored interfaces for Coordinator and Team roles
545 |
546 | The system follows a centralized artifact storage model with event-driven updates to keep all conversations synchronized.
547 |
548 |
549 | === File: assistants/project-assistant/assistant/__init__.py ===
550 | from .chat import app
551 | from .logging import logger, setup_file_logging
552 |
553 | # Set up file logging
554 | log_file = setup_file_logging()
555 | logger.debug(f"Project Assistant initialized with log file: {log_file}")
556 |
557 | __all__ = ["app"]
558 |
559 |
560 | === File: assistants/project-assistant/assistant/chat.py ===
561 | # Copyright (c) Microsoft. All rights reserved.
562 |
563 | # Project Assistant implementation
564 |
565 | import asyncio
566 | import pathlib
567 | from enum import Enum
568 | from typing import Any
569 |
570 | from assistant_extensions import attachments, dashboard_card, navigator
571 | from content_safety.evaluators import CombinedContentSafetyEvaluator
572 | from semantic_workbench_api_model import workbench_model
573 | from semantic_workbench_api_model.workbench_model import (
574 | AssistantStateEvent,
575 | ConversationEvent,
576 | ConversationMessage,
577 | MessageType,
578 | NewConversationMessage,
579 | ParticipantRole,
580 | UpdateParticipant,
581 | )
582 | from semantic_workbench_assistant.assistant_app import (
583 | AssistantApp,
584 | AssistantCapability,
585 | AssistantTemplate,
586 | ContentSafety,
587 | ContentSafetyEvaluator,
588 | ConversationContext,
589 | )
590 |
591 | from assistant.command_processor import command_registry
592 | from assistant.respond import respond_to_conversation
593 | from assistant.team_welcome import generate_team_welcome_message
594 | from assistant.utils import (
595 | DEFAULT_TEMPLATE_ID,
596 | KNOWLEDGE_TRANSFER_TEMPLATE_ID,
597 | is_knowledge_transfer_assistant,
598 | load_text_include,
599 | )
600 |
601 | from .config import assistant_config
602 | from .conversation_project_link import ConversationProjectManager
603 | from .logging import logger
604 | from .project_common import detect_assistant_role
605 | from .project_data import LogEntryType
606 | from .project_files import ProjectFileManager
607 | from .project_manager import ProjectManager
608 | from .project_notifications import ProjectNotifier
609 | from .project_storage import ProjectStorage
610 | from .project_storage_models import ConversationRole
611 | from .state_inspector import ProjectInspectorStateProvider
612 |
613 | service_id = "project-assistant.made-exploration"
614 | service_name = "Project Assistant"
615 | service_description = "A mediator assistant that facilitates file sharing between conversations."
616 |
617 |
618 | async def content_evaluator_factory(
619 | context: ConversationContext,
620 | ) -> ContentSafetyEvaluator:
621 | config = await assistant_config.get(context.assistant)
622 | return CombinedContentSafetyEvaluator(config.content_safety_config)
623 |
624 |
625 | content_safety = ContentSafety(content_evaluator_factory)
626 |
627 | assistant = AssistantApp(
628 | assistant_service_id=service_id,
629 | assistant_service_name=service_name,
630 | assistant_service_description=service_description,
631 | config_provider=assistant_config.provider,
632 | content_interceptor=content_safety,
633 | capabilities={AssistantCapability.supports_conversation_files},
634 | inspector_state_providers={
635 | "project_status": ProjectInspectorStateProvider(assistant_config),
636 | },
637 | additional_templates=[
638 | AssistantTemplate(
639 | id=KNOWLEDGE_TRANSFER_TEMPLATE_ID,
640 | name="Knowledge Transfer Assistant",
641 | description="An assistant for capturing and sharing complex information for others to explore.",
642 | ),
643 | ],
644 | assistant_service_metadata={
645 | **dashboard_card.metadata(
646 | dashboard_card.TemplateConfig(
647 | enabled=False,
648 | template_id=DEFAULT_TEMPLATE_ID,
649 | background_color="rgb(159, 216, 159)",
650 | icon=dashboard_card.image_to_url(
651 | pathlib.Path(__file__).parent / "assets" / "icon.svg", "image/svg+xml"
652 | ),
653 | card_content=dashboard_card.CardContent(
654 | content_type="text/markdown",
655 | content=load_text_include("card_content.md"),
656 | ),
657 | ),
658 | dashboard_card.TemplateConfig(
659 | enabled=True,
660 | template_id=KNOWLEDGE_TRANSFER_TEMPLATE_ID,
661 | icon=dashboard_card.image_to_url(
662 | pathlib.Path(__file__).parent / "assets" / "icon_context_transfer.svg", "image/svg+xml"
663 | ),
664 | background_color="rgb(198,177,222)",
665 | card_content=dashboard_card.CardContent(
666 | content_type="text/markdown",
667 | content=load_text_include("knowledge_transfer_card_content.md"),
668 | ),
669 | ),
670 | ),
671 | **navigator.metadata_for_assistant_navigator({
672 | "default": load_text_include("project_assistant_info.md"),
673 | "knowledge_transfer": load_text_include("knowledge_transfer_assistant_info.md"),
674 | }),
675 | },
676 | )
677 |
678 | attachments_extension = attachments.AttachmentsExtension(assistant)
679 |
680 | app = assistant.fastapi_app()
681 |
682 |
683 | class ConversationType(Enum):
684 | COORDINATOR = "coordinator"
685 | TEAM = "team"
686 | SHAREABLE_TEMPLATE = "shareable_template"
687 |
688 |
689 | @assistant.events.conversation.on_created_including_mine
690 | async def on_conversation_created(context: ConversationContext) -> None:
691 | """
692 | The assistant manages three types of conversations:
693 | 1. Coordinator Conversation: The main conversation used by the project coordinator
694 | 2. Shareable Team Conversation: A template conversation that has a share URL and is never directly used
695 | 3. Team Conversation(s): Individual conversations for team members created when they redeem the share URL
696 | """
697 | # Get conversation to access metadata
698 | conversation = await context.get_conversation()
699 | conversation_metadata = conversation.metadata or {}
700 |
701 | config = await assistant_config.get(context.assistant)
702 |
703 | ##
704 | ## Figure out what type of conversation this is.
705 | ##
706 |
707 | conversation_type = ConversationType.COORDINATOR
708 |
709 | # Coordinator conversations will not have a project_id or
710 | # is_team_conversation flag in the metadata. So, if they are there, we just
711 | # need to decide if it's a shareable template or a team conversation.
712 | project_id = conversation_metadata.get("project_id")
713 | if conversation_metadata.get("is_team_conversation", False) and project_id:
714 | # If this conversation was imported from another, it indicates it's from
715 | # share redemption.
716 | if conversation.imported_from_conversation_id:
717 | conversation_type = ConversationType.TEAM
718 | # TODO: This might work better for detecting a redeemed link, but
719 | # hasn't been validated.
720 |
721 | # if conversation_metadata.get("share_redemption") and conversation_metadata.get("share_redemption").get(
722 | # "conversation_share_id"
723 | # ):
724 | # conversation_type = ConversationType.TEAM
725 | else:
726 | conversation_type = ConversationType.SHAREABLE_TEMPLATE
727 |
728 | ##
729 | ## Handle the conversation based on its type
730 | ##
731 | match conversation_type:
732 | case ConversationType.SHAREABLE_TEMPLATE:
733 | if not project_id:
734 | logger.error("No project ID found for shareable team conversation.")
735 | return
736 |
737 | await ConversationProjectManager.associate_conversation_with_project(context, project_id)
738 | return
739 |
740 | case ConversationType.TEAM:
741 | if not project_id:
742 | logger.error("No project ID found for team conversation.")
743 | return
744 |
745 | # I'd put status messages here, but the attachment's extension is causing race conditions.
746 | await context.send_messages(
747 | NewConversationMessage(
748 | content="Hold on a second while I set up your space...",
749 | message_type=MessageType.chat,
750 | )
751 | )
752 |
753 | await ConversationProjectManager.associate_conversation_with_project(context, project_id)
754 |
755 | # Synchronize files.
756 | await ProjectFileManager.synchronize_files_to_team_conversation(context=context, project_id=project_id)
757 |
758 | # Generate a welcome message.
759 | welcome_message, debug = await generate_team_welcome_message(context)
760 | await context.send_messages(
761 | NewConversationMessage(
762 | content=welcome_message,
763 | message_type=MessageType.chat,
764 | metadata={
765 | "generated_content": True,
766 | "debug": debug,
767 | },
768 | )
769 | )
770 |
771 | # Pop open the inspector panel.
772 | await context.send_conversation_state_event(
773 | AssistantStateEvent(
774 | state_id="project_status",
775 | event="focus",
776 | state=None,
777 | )
778 | )
779 |
780 | return
781 |
782 | case ConversationType.COORDINATOR:
783 | try:
784 | project_id = await ProjectManager.create_project(context)
785 |
786 | # A basic brief to start with.
787 |
788 | await ProjectManager.update_project_brief(
789 | context=context,
790 | title=f"New {config.Project_or_Context}",
791 | description="_This knowledge brief is displayed in the side panel of all of your team members' conversations, too. Before you share links to your team, ask your assistant to update the brief with whatever details you'd like here. What will help your teammates get off to a good start as they explore the knowledge you are sharing?_"
792 | if is_knowledge_transfer_assistant(context)
793 | else "_This project brief is displayed in the side panel of all of your team members' conversations, too. Before you share links to your team, ask your assistant to update the brief with whatever details you'd like here. What will help your teammates get off to a good start as they begin working on your project?_",
794 | )
795 |
796 | # Create a team conversation with a share URL
797 | share_url = await ProjectManager.create_shareable_team_conversation(
798 | context=context, project_id=project_id
799 | )
800 |
801 | welcome_message = config.coordinator_config.welcome_message.format(
802 | share_url=share_url or "<Share URL generation failed>"
803 | )
804 |
805 | except Exception as e:
806 | welcome_message = f"I'm having trouble setting up your project. Please try again or contact support if the issue persists. {str(e)}"
807 |
808 | # Send the welcome message
809 | await context.send_messages(
810 | NewConversationMessage(
811 | content=welcome_message,
812 | message_type=MessageType.chat,
813 | )
814 | )
815 |
816 |
817 | @assistant.events.conversation.message.chat.on_created
818 | async def on_message_created(
819 | context: ConversationContext, event: ConversationEvent, message: ConversationMessage
820 | ) -> None:
821 | await context.update_participant_me(UpdateParticipant(status="thinking..."))
822 |
823 | metadata: dict[str, Any] = {
824 | "debug": {
825 | "content_safety": event.data.get(content_safety.metadata_key, {}),
826 | }
827 | }
828 |
829 | try:
830 | project_id = await ProjectManager.get_project_id(context)
831 | metadata["debug"]["project_id"] = project_id
832 |
833 | # If this is a Coordinator conversation, store the message for Team access
834 | async with context.set_status("jotting..."):
835 | role = await detect_assistant_role(context)
836 | if role == ConversationRole.COORDINATOR and message.message_type == MessageType.chat:
837 | try:
838 | if project_id:
839 | # Get the sender's name
840 | sender_name = "Coordinator"
841 | if message.sender:
842 | participants = await context.get_participants()
843 | for participant in participants.participants:
844 | if participant.id == message.sender.participant_id:
845 | sender_name = participant.name
846 | break
847 |
848 | # Store the message for Team access
849 | ProjectStorage.append_coordinator_message(
850 | project_id=project_id,
851 | message_id=str(message.id),
852 | content=message.content,
853 | sender_name=sender_name,
854 | is_assistant=message.sender.participant_role == ParticipantRole.assistant,
855 | timestamp=message.timestamp,
856 | )
857 | except Exception as e:
858 | # Don't fail message handling if storage fails
859 | logger.exception(f"Error storing Coordinator message for Team access: {e}")
860 |
861 | async with context.set_status("pondering..."):
862 | await respond_to_conversation(
863 | context,
864 | new_message=message,
865 | attachments_extension=attachments_extension,
866 | metadata=metadata,
867 | )
868 |
869 | # If the message is from a Coordinator, update the whiteboard in the background
870 | if role == ConversationRole.COORDINATOR and message.message_type == MessageType.chat:
871 | asyncio.create_task(ProjectManager.auto_update_whiteboard(context))
872 |
873 | except Exception as e:
874 | logger.exception(f"Error handling message: {e}")
875 | await context.send_messages(
876 | NewConversationMessage(
877 | content=f"Error: {str(e)}",
878 | message_type=MessageType.notice,
879 | metadata={"generated_content": False, **metadata},
880 | )
881 | )
882 | finally:
883 | await context.update_participant_me(UpdateParticipant(status=None))
884 |
885 |
886 | @assistant.events.conversation.message.command.on_created
887 | async def on_command_created(
888 | context: ConversationContext, event: ConversationEvent, message: ConversationMessage
889 | ) -> None:
890 | if message.message_type != MessageType.command:
891 | return
892 |
893 | await context.update_participant_me(UpdateParticipant(status="processing command..."))
894 | try:
895 | metadata = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}}
896 |
897 | # Process the command using the command processor
898 | role = await detect_assistant_role(context)
899 | command_processed = await command_registry.process_command(context, message, role.value)
900 |
901 | # If the command wasn't recognized or processed, respond normally
902 | if not command_processed:
903 | await respond_to_conversation(
904 | context,
905 | new_message=message,
906 | attachments_extension=attachments_extension,
907 | metadata=metadata,
908 | )
909 | finally:
910 | # update the participant status to indicate the assistant is done thinking
911 | await context.update_participant_me(UpdateParticipant(status=None))
912 |
913 |
914 | @assistant.events.conversation.file.on_created
915 | async def on_file_created(
916 | context: ConversationContext,
917 | event: workbench_model.ConversationEvent,
918 | file: workbench_model.File,
919 | ) -> None:
920 | """
921 | Handle when a file is created in the conversation.
922 |
923 | For Coordinator files:
924 | 1. Store a copy in project storage
925 | 2. Synchronize to all Team conversations
926 |
927 | For Team files:
928 | 1. Use as-is without copying to project storage
929 | """
930 | try:
931 | project_id = await ProjectManager.get_project_id(context)
932 | if not project_id or not file.filename:
933 | logger.warning(
934 | f"No project ID found or missing filename: project_id={project_id}, filename={file.filename}"
935 | )
936 | return
937 |
938 | role = await detect_assistant_role(context)
939 |
940 | # Use ProjectFileManager for file operations
941 |
942 | # Process based on role
943 | if role == ConversationRole.COORDINATOR:
944 | # For Coordinator files:
945 | # 1. Store in project storage (marked as coordinator file)
946 |
947 | success = await ProjectFileManager.copy_file_to_project_storage(
948 | context=context,
949 | project_id=project_id,
950 | file=file,
951 | is_coordinator_file=True,
952 | )
953 |
954 | if not success:
955 | logger.error(f"Failed to copy file to project storage: {file.filename}")
956 | return
957 |
958 | # 2. Synchronize to all Team conversations
959 | # Get all Team conversations
960 | team_conversations = await ProjectFileManager.get_team_conversations(context, project_id)
961 |
962 | if team_conversations:
963 | for team_conv_id in team_conversations:
964 | await ProjectFileManager.copy_file_to_conversation(
965 | context=context,
966 | project_id=project_id,
967 | filename=file.filename,
968 | target_conversation_id=team_conv_id,
969 | )
970 |
971 | # 3. Update all UIs but don't send notifications to reduce noise
972 | await ProjectNotifier.notify_project_update(
973 | context=context,
974 | project_id=project_id,
975 | update_type="file_created",
976 | message=f"Coordinator shared a file: {file.filename}",
977 | data={"filename": file.filename},
978 | send_notification=False, # Don't send notification to reduce noise
979 | )
980 | # Team files don't need special handling as they're already in the conversation
981 |
982 | # Log file creation to project log for all files
983 | await ProjectStorage.log_project_event(
984 | context=context,
985 | project_id=project_id,
986 | entry_type="file_shared",
987 | message=f"File shared: {file.filename}",
988 | metadata={
989 | "file_id": getattr(file, "id", ""),
990 | "filename": file.filename,
991 | "is_coordinator_file": role.value == "coordinator",
992 | },
993 | )
994 |
995 | except Exception as e:
996 | logger.exception(f"Error handling file creation: {e}")
997 |
998 |
999 | @assistant.events.conversation.file.on_updated
1000 | async def on_file_updated(
1001 | context: ConversationContext,
1002 | event: workbench_model.ConversationEvent,
1003 | file: workbench_model.File,
1004 | ) -> None:
1005 | try:
1006 | # Get project ID
1007 | project_id = await ProjectManager.get_project_id(context)
1008 | if not project_id or not file.filename:
1009 | return
1010 |
1011 | role = await detect_assistant_role(context)
1012 | if role == ConversationRole.COORDINATOR:
1013 | # For Coordinator files:
1014 | # 1. Update in project storage
1015 | success = await ProjectFileManager.copy_file_to_project_storage(
1016 | context=context,
1017 | project_id=project_id,
1018 | file=file,
1019 | is_coordinator_file=True,
1020 | )
1021 |
1022 | if not success:
1023 | logger.error(f"Failed to update file in project storage: {file.filename}")
1024 | return
1025 |
1026 | team_conversations = await ProjectFileManager.get_team_conversations(context, project_id)
1027 | for team_conv_id in team_conversations:
1028 | await ProjectFileManager.copy_file_to_conversation(
1029 | context=context,
1030 | project_id=project_id,
1031 | filename=file.filename,
1032 | target_conversation_id=team_conv_id,
1033 | )
1034 |
1035 | # 3. Update all UIs but don't send notifications to reduce noise
1036 | await ProjectNotifier.notify_project_update(
1037 | context=context,
1038 | project_id=project_id,
1039 | update_type="file_updated",
1040 | message=f"Coordinator updated a file: {file.filename}",
1041 | data={"filename": file.filename},
1042 | send_notification=False, # Don't send notification to reduce noise
1043 | )
1044 | # Team files don't need special handling
1045 |
1046 | # Log file update to project log for all files
1047 | await ProjectStorage.log_project_event(
1048 | context=context,
1049 | project_id=project_id,
1050 | entry_type="file_shared",
1051 | message=f"File updated: {file.filename}",
1052 | metadata={
1053 | "file_id": getattr(file, "id", ""),
1054 | "filename": file.filename,
1055 | "is_coordinator_file": role.value == "coordinator",
1056 | },
1057 | )
1058 |
1059 | except Exception as e:
1060 | logger.exception(f"Error handling file update: {e}")
1061 |
1062 |
1063 | @assistant.events.conversation.file.on_deleted
1064 | async def on_file_deleted(
1065 | context: ConversationContext,
1066 | event: workbench_model.ConversationEvent,
1067 | file: workbench_model.File,
1068 | ) -> None:
1069 | try:
1070 | # Get project ID
1071 | project_id = await ProjectManager.get_project_id(context)
1072 | if not project_id or not file.filename:
1073 | return
1074 |
1075 | role = await detect_assistant_role(context)
1076 | if role == ConversationRole.COORDINATOR:
1077 | # For Coordinator files:
1078 | # 1. Delete from project storage
1079 | success = await ProjectFileManager.delete_file_from_project_storage(
1080 | context=context, project_id=project_id, filename=file.filename
1081 | )
1082 |
1083 | if not success:
1084 | logger.error(f"Failed to delete file from project storage: {file.filename}")
1085 |
1086 | # 2. Update all UIs about the deletion but don't send notifications to reduce noise
1087 | await ProjectNotifier.notify_project_update(
1088 | context=context,
1089 | project_id=project_id,
1090 | update_type="file_deleted",
1091 | message=f"Coordinator deleted a file: {file.filename}",
1092 | data={"filename": file.filename},
1093 | send_notification=False, # Don't send notification to reduce noise
1094 | )
1095 | # Team files don't need special handling
1096 |
1097 | # Log file deletion to project log for all files
1098 | await ProjectStorage.log_project_event(
1099 | context=context,
1100 | project_id=project_id,
1101 | entry_type="file_deleted",
1102 | message=f"File deleted: {file.filename}",
1103 | metadata={
1104 | "file_id": getattr(file, "id", ""),
1105 | "filename": file.filename,
1106 | "is_coordinator_file": role.value == "coordinator",
1107 | },
1108 | )
1109 |
1110 | except Exception as e:
1111 | logger.exception(f"Error handling file deletion: {e}")
1112 |
1113 |
1114 | @assistant.events.conversation.participant.on_created
1115 | async def on_participant_joined(
1116 | context: ConversationContext,
1117 | event: ConversationEvent,
1118 | participant: workbench_model.ConversationParticipant,
1119 | ) -> None:
1120 | try:
1121 | if participant.id == context.assistant.id:
1122 | return
1123 |
1124 | # Open the Brief tab (state inspector).
1125 | await context.send_conversation_state_event(
1126 | AssistantStateEvent(
1127 | state_id="project_status",
1128 | event="focus",
1129 | state=None,
1130 | )
1131 | )
1132 |
1133 | role = await detect_assistant_role(context)
1134 | if role != ConversationRole.TEAM:
1135 | return
1136 |
1137 | project_id = await ConversationProjectManager.get_associated_project_id(context)
1138 | if not project_id:
1139 | return
1140 |
1141 | await ProjectFileManager.synchronize_files_to_team_conversation(context=context, project_id=project_id)
1142 |
1143 | await ProjectStorage.log_project_event(
1144 | context=context,
1145 | project_id=project_id,
1146 | entry_type=LogEntryType.PARTICIPANT_JOINED,
1147 | message=f"Participant joined: {participant.name}",
1148 | metadata={
1149 | "participant_id": participant.id,
1150 | "participant_name": participant.name,
1151 | "conversation_id": str(context.id),
1152 | },
1153 | )
1154 |
1155 | except Exception as e:
1156 | logger.exception(f"Error handling participant join event: {e}")
1157 |
1158 |
1159 | === File: assistants/project-assistant/assistant/command_processor.py ===
1160 | """
1161 | Command processor for the project assistant.
1162 |
1163 | This module provides a unified framework for processing commands in the project assistant.
1164 | It defines a command registry, command handlers for both Coordinator and Team modes, and authorization
1165 | controls based on user roles.
1166 | """
1167 |
1168 | import logging
1169 | from typing import Any, Awaitable, Callable, Dict, List, Optional
1170 |
1171 | from semantic_workbench_api_model.workbench_model import (
1172 | ConversationMessage,
1173 | MessageType,
1174 | NewConversationMessage,
1175 | )
1176 | from semantic_workbench_assistant.assistant_app import ConversationContext
1177 |
1178 | from .conversation_project_link import ConversationProjectManager
1179 | from .project_data import (
1180 | RequestPriority,
1181 | RequestStatus,
1182 | )
1183 | from .project_manager import ProjectManager
1184 | from .project_notifications import ProjectNotifier
1185 | from .project_storage import ProjectStorage
1186 | from .project_storage_models import ConversationRole
1187 |
1188 | logger = logging.getLogger(__name__)
1189 |
1190 | # Command handler function type
1191 | CommandHandlerType = Callable[[ConversationContext, ConversationMessage, List[str]], Awaitable[None]]
1192 |
1193 |
1194 | class CommandRegistry:
1195 | """Registry for command handlers with authorization controls."""
1196 |
1197 | def __init__(self):
1198 | """Initialize the command registry."""
1199 | self.commands: Dict[str, Dict[str, Any]] = {}
1200 |
1201 | def register_command(
1202 | self,
1203 | command_name: str,
1204 | handler: CommandHandlerType,
1205 | description: str,
1206 | usage: str,
1207 | example: str,
1208 | authorized_roles: Optional[List[str]] = None,
1209 | ) -> None:
1210 | """
1211 | Register a command handler.
1212 |
1213 | Args:
1214 | command_name: The command name (without the '/')
1215 | handler: The function that handles the command
1216 | description: A brief description of what the command does
1217 | usage: The command usage format
1218 | example: An example of how to use the command
1219 | authorized_roles: List of roles that can use this command (None for all)
1220 | """
1221 | self.commands[command_name] = {
1222 | "handler": handler,
1223 | "description": description,
1224 | "usage": usage,
1225 | "example": example,
1226 | "authorized_roles": authorized_roles,
1227 | }
1228 |
1229 | def is_authorized(self, command_name: str, role: str) -> bool:
1230 | """
1231 | Check if a role is authorized to use a command.
1232 |
1233 | Args:
1234 | command_name: The command name
1235 | role: The user role
1236 |
1237 | Returns:
1238 | True if authorized, False otherwise
1239 | """
1240 | if command_name not in self.commands:
1241 | return False
1242 |
1243 | authorized_roles = self.commands[command_name]["authorized_roles"]
1244 | if authorized_roles is None:
1245 | return True # Command available to all roles
1246 |
1247 | return role in authorized_roles
1248 |
1249 | def get_command_help(self, command_name: str) -> Optional[Dict[str, str]]:
1250 | """
1251 | Get help information for a command.
1252 |
1253 | Args:
1254 | command_name: The command name
1255 |
1256 | Returns:
1257 | Dictionary with help information or None if command not found
1258 | """
1259 | if command_name not in self.commands:
1260 | return None
1261 |
1262 | return {
1263 | "description": self.commands[command_name]["description"],
1264 | "usage": self.commands[command_name]["usage"],
1265 | "example": self.commands[command_name]["example"],
1266 | }
1267 |
1268 | def get_commands_for_role(self, role: str) -> Dict[str, Dict[str, Any]]:
1269 | """
1270 | Get all commands available for a specific role.
1271 |
1272 | Args:
1273 | role: The user role
1274 |
1275 | Returns:
1276 | Dictionary of commands available to the role
1277 | """
1278 | return {name: cmd for name, cmd in self.commands.items() if self.is_authorized(name, role)}
1279 |
1280 | async def process_command(self, context: ConversationContext, message: ConversationMessage, role: str) -> bool:
1281 | """
1282 | Process a command message.
1283 |
1284 | Args:
1285 | context: The conversation context
1286 | message: The command message
1287 | role: The user's role (coordinator or team)
1288 |
1289 | Returns:
1290 | True if command was processed, False otherwise
1291 | """
1292 | # Split the command and extract the command name
1293 | content = message.content.strip()
1294 | if not content.startswith("/"):
1295 | return False
1296 |
1297 | # Extract command name and arguments
1298 | parts = content.split()
1299 | command_name = parts[0][1:] # Remove the '/' prefix
1300 | args = parts[1:] if len(parts) > 1 else []
1301 |
1302 | # Check if command exists
1303 | if command_name not in self.commands:
1304 | await context.send_messages(
1305 | NewConversationMessage(
1306 | content=f"Unknown command: /{command_name}. Type /help to see available commands.",
1307 | message_type=MessageType.notice,
1308 | )
1309 | )
1310 | return True
1311 |
1312 | # Check if user is authorized to use this command
1313 | if not self.is_authorized(command_name, role):
1314 | await context.send_messages(
1315 | NewConversationMessage(
1316 | content=f"The /{command_name} command is only available to {' or '.join(self.commands[command_name]['authorized_roles'])} roles. You are in {role.upper()} mode.",
1317 | message_type=MessageType.notice,
1318 | )
1319 | )
1320 | return True
1321 |
1322 | try:
1323 | # Execute the command handler
1324 | await self.commands[command_name]["handler"](context, message, args)
1325 | return True
1326 | except Exception as e:
1327 | logger.exception(f"Error processing command /{command_name}: {e}")
1328 | await context.send_messages(
1329 | NewConversationMessage(
1330 | content=f"Error processing command /{command_name}: {str(e)}",
1331 | message_type=MessageType.notice,
1332 | )
1333 | )
1334 | return True
1335 |
1336 |
1337 | # Initialize the command registry
1338 | command_registry = CommandRegistry()
1339 |
1340 |
1341 | # Command handler implementations
1342 |
1343 |
1344 | async def handle_help_command(context: ConversationContext, message: ConversationMessage, args: List[str]) -> None:
1345 | """Handle the help command."""
1346 | # Get the conversation's role
1347 | from .conversation_project_link import ConversationProjectManager
1348 |
1349 | # First check conversation metadata
1350 | conversation = await context.get_conversation()
1351 | metadata = conversation.metadata or {}
1352 | setup_complete = metadata.get("setup_complete", False)
1353 | assistant_mode = metadata.get("assistant_mode", "setup")
1354 | metadata_role = metadata.get("project_role")
1355 |
1356 | # First check if project ID exists - if it does, setup should be considered complete
1357 | project_id = await ProjectManager.get_project_id(context)
1358 | if project_id:
1359 | # If we have a project ID, we should never show the setup instructions
1360 | setup_complete = True
1361 |
1362 | # If metadata doesn't reflect this, try to get actual role
1363 | if not metadata.get("setup_complete", False):
1364 | role = await ConversationProjectManager.get_conversation_role(context)
1365 | if role:
1366 | metadata_role = role.value
1367 | else:
1368 | # Default to team mode if we can't determine role
1369 | metadata_role = "team"
1370 |
1371 | # Special handling for setup mode - only if we truly have no project
1372 | if not setup_complete and assistant_mode == "setup" and not project_id:
1373 | # If a specific command is specified, show detailed help for that command
1374 | if args:
1375 | command_name = args[0]
1376 | if command_name.startswith("/"):
1377 | command_name = command_name[1:] # Remove the '/' prefix
1378 |
1379 | # For setup mode, only show help for setup commands
1380 | setup_commands = ["start-coordinator", "join", "help"]
1381 |
1382 | if command_name in setup_commands:
1383 | help_info = command_registry.get_command_help(command_name)
1384 | if help_info:
1385 | await context.send_messages(
1386 | NewConversationMessage(
1387 | content=f"""## Help: /{command_name}
1388 |
1389 | {help_info["description"]}
1390 |
1391 | **Usage:** {help_info["usage"]}
1392 |
1393 | **Example:** {help_info["example"]}
1394 | """,
1395 | message_type=MessageType.chat,
1396 | )
1397 | )
1398 | return
1399 |
1400 | # If not a setup command, show generic message
1401 | await context.send_messages(
1402 | NewConversationMessage(
1403 | content=f"The /{command_name} command is not available in setup mode. Please first use `/start-coordinator` or `/join` to establish your role.",
1404 | message_type=MessageType.notice,
1405 | )
1406 | )
1407 | return
1408 |
1409 | # Show setup-specific help
1410 | help_text = """## Project Assistant
1411 |
1412 | This assistant is automatically set up to help you with your project:
1413 |
1414 | - As a Coordinator: This conversation is your personal conversation for managing the project
1415 | - As a Team Member: This conversation is for collaborating on the project with others
1416 |
1417 | No setup commands needed! You're already good to go.
1418 |
1419 | Type `/help` to see all available commands for your role.
1420 | """
1421 |
1422 | await context.send_messages(
1423 | NewConversationMessage(
1424 | content=help_text,
1425 | message_type=MessageType.chat,
1426 | )
1427 | )
1428 | return
1429 |
1430 | # Normal (non-setup) help processing
1431 | # Use the role from metadata, which is always the authoritative source
1432 | # Log the role for debugging
1433 | logger.debug(f"Role detection in help command - Metadata role: {metadata_role}")
1434 |
1435 | # Use the role from metadata or default to coordinator
1436 | role = metadata_role or "coordinator" # Default to coordinator if not set
1437 |
1438 | # If a specific command is specified, show detailed help for that command
1439 | if args:
1440 | command_name = args[0]
1441 | if command_name.startswith("/"):
1442 | command_name = command_name[1:] # Remove the '/' prefix
1443 |
1444 | help_info = command_registry.get_command_help(command_name)
1445 |
1446 | if help_info and command_registry.is_authorized(command_name, role):
1447 | await context.send_messages(
1448 | NewConversationMessage(
1449 | content=f"""## Help: /{command_name}
1450 |
1451 | {help_info["description"]}
1452 |
1453 | **Usage:** {help_info["usage"]}
1454 |
1455 | **Example:** {help_info["example"]}
1456 | """,
1457 | message_type=MessageType.chat,
1458 | )
1459 | )
1460 | else:
1461 | await context.send_messages(
1462 | NewConversationMessage(
1463 | content=f"No help available for command /{command_name} or you're not authorized to use it.",
1464 | message_type=MessageType.notice,
1465 | )
1466 | )
1467 | return
1468 |
1469 | # Otherwise show all available commands for the current role
1470 | available_commands = command_registry.get_commands_for_role(role)
1471 |
1472 | # Format help text based on role
1473 | if role == ConversationRole.COORDINATOR.value:
1474 | help_text = "## Assistant Commands (Coordinator Mode)\n\n"
1475 | else:
1476 | help_text = "## Assistant Commands (Team Mode)\n\n"
1477 |
1478 | # Group commands by category
1479 | project_commands = []
1480 | whiteboard_commands = []
1481 | request_commands = []
1482 | team_commands = []
1483 | status_commands = []
1484 | info_commands = []
1485 |
1486 | for name, cmd in available_commands.items():
1487 | command_entry = f"- `/{name}`: {cmd['description']}"
1488 |
1489 | if "create-brief" in name or "add-goal" in name:
1490 | project_commands.append(command_entry)
1491 | elif "whiteboard" in name:
1492 | whiteboard_commands.append(command_entry)
1493 | elif "request" in name:
1494 | request_commands.append(command_entry)
1495 | elif "invite" in name or "join" in name or "list-participants" in name:
1496 | team_commands.append(command_entry)
1497 | elif "status" in name or "update" in name:
1498 | status_commands.append(command_entry)
1499 | else:
1500 | info_commands.append(command_entry)
1501 |
1502 | # Add sections to help text if they have commands
1503 | if project_commands:
1504 | help_text += "### Project Configuration\n" + "\n".join(project_commands) + "\n\n"
1505 |
1506 | if whiteboard_commands:
1507 | help_text += "### Whiteboard Management\n" + "\n".join(whiteboard_commands) + "\n\n"
1508 |
1509 | if team_commands:
1510 | help_text += "### Team Management\n" + "\n".join(team_commands) + "\n\n"
1511 |
1512 | if request_commands:
1513 | help_text += "### Information Request Management\n" + "\n".join(request_commands) + "\n\n"
1514 |
1515 | if status_commands:
1516 | help_text += "### Status Management\n" + "\n".join(status_commands) + "\n\n"
1517 |
1518 | if info_commands:
1519 | help_text += "### Information\n" + "\n".join(info_commands) + "\n\n"
1520 |
1521 | # Add role-specific guidance
1522 | if role == ConversationRole.COORDINATOR.value:
1523 | help_text += (
1524 | "As a Coordinator, you are responsible for defining the project and responding to team member requests."
1525 | )
1526 | else:
1527 | help_text += "As a Team member, you can access project information, request information, and report progress on project goals."
1528 |
1529 | await context.send_messages(
1530 | NewConversationMessage(
1531 | content=help_text,
1532 | message_type=MessageType.chat,
1533 | )
1534 | )
1535 |
1536 |
1537 | async def handle_create_brief_command(
1538 | context: ConversationContext, message: ConversationMessage, args: List[str]
1539 | ) -> None:
1540 | """Handle the create-brief command."""
1541 | # Parse the command
1542 | content = message.content.strip()[len("/create-brief") :].strip()
1543 |
1544 | if not content or "|" not in content:
1545 | await context.send_messages(
1546 | NewConversationMessage(
1547 | content="Please provide a brief title and description in the format: `/create-brief Title|Description here`",
1548 | message_type=MessageType.notice,
1549 | )
1550 | )
1551 | return
1552 |
1553 | # Extract title and description
1554 | try:
1555 | title, description = content.split("|", 1)
1556 | title = title.strip()
1557 | description = description.strip()
1558 |
1559 | if not title or not description:
1560 | raise ValueError("Both name and description are required")
1561 |
1562 | # Create the brief without sending a notification (we'll send our own)
1563 | briefing = await ProjectManager.update_project_brief(context, title, description, send_notification=False)
1564 |
1565 | if briefing:
1566 | await context.send_messages(
1567 | NewConversationMessage(
1568 | content=f"Brief '{title}' updated successfully.",
1569 | message_type=MessageType.chat,
1570 | )
1571 | )
1572 | else:
1573 | await context.send_messages(
1574 | NewConversationMessage(
1575 | content="Failed to update brief. Please try again.",
1576 | message_type=MessageType.notice,
1577 | )
1578 | )
1579 | except Exception as e:
1580 | logger.exception(f"Error updating brief: {e}")
1581 | await context.send_messages(
1582 | NewConversationMessage(
1583 | content=f"Error updating brief: {str(e)}",
1584 | message_type=MessageType.notice,
1585 | )
1586 | )
1587 |
1588 |
1589 | async def handle_add_goal_command(context: ConversationContext, message: ConversationMessage, args: List[str]) -> None:
1590 | """Handle the add-goal command."""
1591 | # Parse the command
1592 | content = message.content.strip()[len("/add-goal") :].strip()
1593 |
1594 | if not content or "|" not in content:
1595 | await context.send_messages(
1596 | NewConversationMessage(
1597 | content="Please provide a goal name, description, and success criteria in the format: `/add-goal Goal Name|Goal description|Success criteria 1;Success criteria 2`",
1598 | message_type=MessageType.notice,
1599 | )
1600 | )
1601 | return
1602 |
1603 | # Extract goal details
1604 | try:
1605 | parts = content.split("|")
1606 |
1607 | if len(parts) < 2:
1608 | raise ValueError("Goal name and description are required")
1609 |
1610 | goal_name = parts[0].strip()
1611 | goal_description = parts[1].strip()
1612 |
1613 | # Parse success criteria if provided
1614 | success_criteria = []
1615 | if len(parts) > 2 and parts[2].strip():
1616 | criteria_list = parts[2].strip().split(";")
1617 | success_criteria = [c.strip() for c in criteria_list if c.strip()]
1618 |
1619 | if not goal_name or not goal_description:
1620 | raise ValueError("Both goal name and description are required")
1621 |
1622 | # Get project ID
1623 | project_id = await ConversationProjectManager.get_associated_project_id(context)
1624 | if not project_id:
1625 | await context.send_messages(
1626 | NewConversationMessage(
1627 | content="You are not associated with a project. Please create one first with `/create-brief`.",
1628 | message_type=MessageType.notice,
1629 | )
1630 | )
1631 | return
1632 |
1633 | # Use the dedicated method to add a goal to the project
1634 | goal = await ProjectManager.add_project_goal(
1635 | context=context,
1636 | goal_name=goal_name,
1637 | goal_description=goal_description,
1638 | success_criteria=success_criteria,
1639 | )
1640 |
1641 | if goal:
1642 | # Notify all linked conversations about the update
1643 | await ProjectNotifier.notify_project_update(
1644 | context=context,
1645 | project_id=project_id,
1646 | update_type="briefing",
1647 | message=f"Goal added to project: {goal_name}",
1648 | )
1649 |
1650 | # Build success criteria message
1651 | criteria_msg = ""
1652 | if success_criteria:
1653 | criteria_list = "\n".join([f"- {c}" for c in success_criteria])
1654 | criteria_msg = f"\n\nSuccess Criteria:\n{criteria_list}"
1655 |
1656 | await context.send_messages(
1657 | NewConversationMessage(
1658 | content=f"Goal '{goal_name}' added successfully.{criteria_msg}",
1659 | message_type=MessageType.chat,
1660 | )
1661 | )
1662 | else:
1663 | await context.send_messages(
1664 | NewConversationMessage(
1665 | content="Failed to add new goal. Please try again.",
1666 | message_type=MessageType.notice,
1667 | )
1668 | )
1669 | except Exception as e:
1670 | logger.exception(f"Error adding goal: {e}")
1671 | await context.send_messages(
1672 | NewConversationMessage(
1673 | content=f"Error adding goal: {str(e)}",
1674 | message_type=MessageType.notice,
1675 | )
1676 | )
1677 |
1678 |
1679 | async def handle_request_info_command(
1680 | context: ConversationContext, message: ConversationMessage, args: List[str]
1681 | ) -> None:
1682 | """Handle the request-info command."""
1683 | # Parse the command
1684 | content = message.content.strip()[len("/request-info") :].strip()
1685 |
1686 | if not content or "|" not in content:
1687 | await context.send_messages(
1688 | NewConversationMessage(
1689 | content="Please provide a request title and description in the format: `/request-info Request Title|Description of what you need|priority` (priority is optional: low, medium, high, critical)",
1690 | message_type=MessageType.notice,
1691 | )
1692 | )
1693 | return
1694 |
1695 | # Extract request details
1696 | try:
1697 | parts = content.split("|")
1698 |
1699 | title = parts[0].strip()
1700 | description = parts[1].strip() if len(parts) > 1 else ""
1701 | priority_str = parts[2].strip().lower() if len(parts) > 2 else "medium"
1702 |
1703 | if not title or not description:
1704 | raise ValueError("Both request title and description are required")
1705 |
1706 | # Map priority string to enum
1707 | priority_map = {
1708 | "low": RequestPriority.LOW,
1709 | "medium": RequestPriority.MEDIUM,
1710 | "high": RequestPriority.HIGH,
1711 | "critical": RequestPriority.CRITICAL,
1712 | }
1713 | priority = priority_map.get(priority_str, RequestPriority.MEDIUM)
1714 |
1715 | # Create the information request
1716 | success, request = await ProjectManager.create_information_request(
1717 | context=context, title=title, description=description, priority=priority
1718 | )
1719 |
1720 | if success and request:
1721 | await context.send_messages(
1722 | NewConversationMessage(
1723 | content=f"Information request '{title}' created successfully with {priority_str} priority. The Coordinator has been notified and will respond to your request.",
1724 | message_type=MessageType.chat,
1725 | )
1726 | )
1727 | else:
1728 | await context.send_messages(
1729 | NewConversationMessage(
1730 | content="Failed to create information request. Please try again.",
1731 | message_type=MessageType.notice,
1732 | )
1733 | )
1734 | except Exception as e:
1735 | logger.exception(f"Error creating information request: {e}")
1736 | await context.send_messages(
1737 | NewConversationMessage(
1738 | content=f"Error creating information request: {str(e)}",
1739 | message_type=MessageType.notice,
1740 | )
1741 | )
1742 |
1743 |
1744 | async def handle_update_status_command(
1745 | context: ConversationContext, message: ConversationMessage, args: List[str]
1746 | ) -> None:
1747 | """Handle the update-status command."""
1748 | # Parse the command
1749 | content = message.content.strip()[len("/update-status") :].strip()
1750 |
1751 | if not content:
1752 | await context.send_messages(
1753 | NewConversationMessage(
1754 | content="Please provide status information in the format: `/update-status status|progress|message` (e.g., `/update-status in_progress|75|Making good progress on objectives`)",
1755 | message_type=MessageType.notice,
1756 | )
1757 | )
1758 | return
1759 |
1760 | # Extract status details
1761 | try:
1762 | parts = content.split("|")
1763 |
1764 | status = parts[0].strip() if parts else None
1765 | progress_str = parts[1].strip() if len(parts) > 1 else None
1766 | status_message = parts[2].strip() if len(parts) > 2 else None
1767 |
1768 | # Convert progress to int if provided
1769 | progress = None
1770 | if progress_str:
1771 | try:
1772 | progress = int(progress_str)
1773 | # Ensure progress is between 0-100
1774 | progress = max(0, min(100, progress))
1775 | except ValueError:
1776 | progress = None
1777 |
1778 | # Update the project status
1779 | success, status_obj = await ProjectManager.update_project_state(
1780 | context=context, state=status, status_message=status_message
1781 | )
1782 |
1783 | if success and status_obj:
1784 | # Format progress as percentage if available
1785 | progress_text = f" ({progress}% complete)" if progress is not None else ""
1786 |
1787 | await context.send_messages(
1788 | NewConversationMessage(
1789 | content=f"Project status updated to '{status}'{progress_text}. All project participants will see this update.",
1790 | message_type=MessageType.chat,
1791 | )
1792 | )
1793 | else:
1794 | await context.send_messages(
1795 | NewConversationMessage(
1796 | content="Failed to update project status. Please try again.",
1797 | message_type=MessageType.notice,
1798 | )
1799 | )
1800 | except Exception as e:
1801 | logger.exception(f"Error updating project status: {e}")
1802 | await context.send_messages(
1803 | NewConversationMessage(
1804 | content=f"Error updating project status: {str(e)}",
1805 | message_type=MessageType.notice,
1806 | )
1807 | )
1808 |
1809 |
1810 | async def handle_resolve_request_command(
1811 | context: ConversationContext, message: ConversationMessage, args: List[str]
1812 | ) -> None:
1813 | """Handle the resolve-request command."""
1814 | # Parse the command
1815 | content = message.content.strip()[len("/resolve-request") :].strip()
1816 |
1817 | if not content or "|" not in content:
1818 | await context.send_messages(
1819 | NewConversationMessage(
1820 | content="Please provide a request ID and resolution in the format: `/resolve-request request_id|Resolution information here`",
1821 | message_type=MessageType.notice,
1822 | )
1823 | )
1824 | return
1825 |
1826 | try:
1827 | # Extract request ID and resolution
1828 | request_id, resolution = content.split("|", 1)
1829 | request_id = request_id.strip()
1830 | resolution = resolution.strip()
1831 |
1832 | if not request_id or not resolution:
1833 | raise ValueError("Both request ID and resolution are required")
1834 |
1835 | # Show all information requests if the user doesn't know the ID
1836 | if request_id.lower() == "list":
1837 | await context.send_messages(
1838 | NewConversationMessage(
1839 | content="Here are the active information requests:",
1840 | message_type=MessageType.notice,
1841 | )
1842 | )
1843 |
1844 | # Get information requests
1845 | requests = await ProjectManager.get_information_requests(context)
1846 |
1847 | # Filter for active requests
1848 | active_requests = [r for r in requests if r.status != RequestStatus.RESOLVED]
1849 |
1850 | if active_requests:
1851 | request_list = ["## Active Information Requests\n"]
1852 |
1853 | for request in active_requests:
1854 | request_list.append(f"**ID**: `{request.request_id}`")
1855 | request_list.append(f"**Title**: {request.title}")
1856 | request_list.append(f"**Priority**: {request.priority.value}")
1857 | request_list.append(f"**Description**: {request.description}")
1858 | request_list.append("")
1859 |
1860 | await context.send_messages(
1861 | NewConversationMessage(
1862 | content="\n".join(request_list),
1863 | message_type=MessageType.chat,
1864 | )
1865 | )
1866 | else:
1867 | await context.send_messages(
1868 | NewConversationMessage(
1869 | content="No active information requests found.",
1870 | message_type=MessageType.notice,
1871 | )
1872 | )
1873 | return
1874 |
1875 | # Resolve the information request
1876 | success, info_request = await ProjectManager.resolve_information_request(
1877 | context=context, request_id=request_id, resolution=resolution
1878 | )
1879 |
1880 | if success and info_request:
1881 | await context.send_messages(
1882 | NewConversationMessage(
1883 | content=f"Information request '{info_request.title}' has been resolved. The Team has been notified.",
1884 | message_type=MessageType.chat,
1885 | )
1886 | )
1887 | else:
1888 | await context.send_messages(
1889 | NewConversationMessage(
1890 | content="Failed to resolve the information request. Make sure the request ID is correct and the request is not already resolved.",
1891 | message_type=MessageType.notice,
1892 | )
1893 | )
1894 |
1895 | # Suggest listing all requests to help the user
1896 | await context.send_messages(
1897 | NewConversationMessage(
1898 | content="Use `/resolve-request list|` to view all information requests and their IDs.",
1899 | message_type=MessageType.notice,
1900 | )
1901 | )
1902 | except Exception as e:
1903 | logger.exception(f"Error resolving information request: {e}")
1904 | await context.send_messages(
1905 | NewConversationMessage(
1906 | content=f"Error resolving information request: {str(e)}",
1907 | message_type=MessageType.notice,
1908 | )
1909 | )
1910 |
1911 |
1912 | async def handle_project_info_command(
1913 | context: ConversationContext, message: ConversationMessage, args: List[str]
1914 | ) -> None:
1915 | """Handle the project-info command."""
1916 | # Parse the command
1917 | content = " ".join(args).strip().lower()
1918 |
1919 | try:
1920 | # Determine which information to show
1921 | info_type = content if content else "all"
1922 |
1923 | if info_type not in ["all", "brief", "whiteboard", "status", "requests"]:
1924 | await context.send_messages(
1925 | NewConversationMessage(
1926 | content="Please specify what information you want to see: `/project-info [brief|whiteboard|status|requests]`",
1927 | message_type=MessageType.notice,
1928 | )
1929 | )
1930 | return
1931 |
1932 | # Get the requested information
1933 | output = []
1934 |
1935 | # Always show project ID at the top for easy access
1936 | project_id = await ProjectManager.get_project_id(context)
1937 | if project_id:
1938 | # Check if Coordinator or Team
1939 | role = await ProjectManager.get_project_role(context)
1940 | if role == ConversationRole.COORDINATOR:
1941 | # For Coordinator, make it prominent with instructions
1942 | output.append(f"## Project ID: `{project_id}`")
1943 | output.append(f"_Share this ID with team members so they can join using_ `/join {project_id}`\n")
1944 | else:
1945 | # For Team, just show the ID
1946 | output.append(f"## Project ID: `{project_id}`\n")
1947 |
1948 | # Get brief if requested
1949 | if info_type in ["all", "brief"]:
1950 | briefing = await ProjectManager.get_project_brief(context)
1951 |
1952 | if briefing:
1953 | # Format briefing information
1954 | output.append(f"## Brief: {briefing.title}")
1955 | output.append(f"\n{briefing.description}\n")
1956 |
1957 | # Get project to access goals
1958 | if project_id:
1959 | project = ProjectStorage.read_project(project_id)
1960 | if project and project.goals:
1961 | output.append("\n### Goals:\n")
1962 |
1963 | for i, goal in enumerate(project.goals):
1964 | # Count completed criteria
1965 | completed = sum(1 for c in goal.success_criteria if c.completed)
1966 | total = len(goal.success_criteria)
1967 |
1968 | output.append(f"{i + 1}. **{goal.name}** - {goal.description}")
1969 |
1970 | if goal.success_criteria:
1971 | output.append(f" Progress: {completed}/{total} criteria complete")
1972 | output.append(" Success Criteria:")
1973 |
1974 | for j, criterion in enumerate(goal.success_criteria):
1975 | status = "✅" if criterion.completed else "⬜"
1976 | output.append(f" {status} {criterion.description}")
1977 |
1978 | output.append("")
1979 |
1980 | # Get project whiteboard if requested
1981 | if info_type in ["all", "whiteboard"]:
1982 | whiteboard = await ProjectManager.get_project_whiteboard(context)
1983 |
1984 | if whiteboard and whiteboard.content:
1985 | output.append("\n## Project Whiteboard\n")
1986 | output.append(whiteboard.content)
1987 | output.append("")
1988 |
1989 | if whiteboard.is_auto_generated:
1990 | output.append("*This whiteboard content is automatically updated by the assistant.*")
1991 | else:
1992 | output.append("*This whiteboard content has been manually edited.*")
1993 |
1994 | output.append("")
1995 | elif info_type == "whiteboard":
1996 | output.append("\n## Project Whiteboard\n")
1997 | output.append(
1998 | "*No whiteboard content available yet. Content will be automatically generated as the project progresses.*"
1999 | )
2000 |
2001 | # Get project status if requested
2002 | if info_type in ["all", "status"]:
2003 | project_info = await ProjectManager.get_project_info(context)
2004 |
2005 | if project_info:
2006 | output.append("\n## Project Status\n")
2007 | output.append(f"**Current Status**: {project_info.state.value}")
2008 |
2009 | if project_info.status_message:
2010 | output.append(f"**Status Message**: {project_info.status_message}")
2011 |
2012 | # Success criteria status can be calculated from the brief if needed later
2013 | elif info_type == "status":
2014 | output.append("\n## Project Status\n")
2015 | output.append("*No project status defined yet. Update status with `/update-status`.*")
2016 |
2017 | # Get information requests if requested
2018 | if info_type in ["all", "requests"]:
2019 | requests = await ProjectManager.get_information_requests(context)
2020 |
2021 | if requests:
2022 | output.append("\n## Information Requests\n")
2023 |
2024 | # Group requests by status
2025 | active_requests = [r for r in requests if r.status != RequestStatus.RESOLVED]
2026 | resolved_requests = [r for r in requests if r.status == RequestStatus.RESOLVED]
2027 |
2028 | if active_requests:
2029 | output.append("### Active Requests\n")
2030 |
2031 | for request in active_requests:
2032 | priority_marker = {
2033 | RequestPriority.LOW.value: "🔹",
2034 | RequestPriority.MEDIUM.value: "🔶",
2035 | RequestPriority.HIGH.value: "🔴",
2036 | RequestPriority.CRITICAL.value: "⚠️",
2037 | }.get(request.priority.value, "🔹")
2038 |
2039 | # Include request ID for easy reference when resolving
2040 | output.append(f"{priority_marker} **{request.title}** ({request.status.value})")
2041 | output.append(f" ID: `{request.request_id}`")
2042 | output.append(f" {request.description}")
2043 |
2044 | if request.updates:
2045 | last_update = request.updates[-1]
2046 | output.append(f" *Last update: {last_update.get('message', '')}*")
2047 |
2048 | output.append("")
2049 |
2050 | if resolved_requests and info_type == "requests":
2051 | output.append("### Resolved Requests\n")
2052 |
2053 | for request in resolved_requests[:5]: # Show only the 5 most recent
2054 | output.append(f"✅ **{request.title}** ({request.status.value})")
2055 | output.append(f" ID: `{request.request_id}`")
2056 |
2057 | if request.resolution:
2058 | output.append(f" Resolution: {request.resolution}")
2059 |
2060 | output.append("")
2061 | elif info_type == "requests":
2062 | output.append("\n## Information Requests\n")
2063 | output.append("*No information requests created yet. Request information with `/request-info`.*")
2064 |
2065 | # If no data was found for any category
2066 | if not output:
2067 | output.append("No project information found. Start by creating a brief with `/create-brief`.")
2068 |
2069 | # Send the formatted information
2070 | await context.send_messages(
2071 | NewConversationMessage(
2072 | content="\n".join(output),
2073 | message_type=MessageType.chat,
2074 | )
2075 | )
2076 |
2077 | except Exception as e:
2078 | logger.exception(f"Error displaying project info: {e}")
2079 | await context.send_messages(
2080 | NewConversationMessage(
2081 | content=f"Error displaying project information: {str(e)}",
2082 | message_type=MessageType.notice,
2083 | )
2084 | )
2085 |
2086 |
2087 | async def handle_list_participants_command(
2088 | context: ConversationContext, message: ConversationMessage, args: List[str]
2089 | ) -> None:
2090 | """Handle the list-participants command."""
2091 | try:
2092 | # Get project ID
2093 | project_id = await ConversationProjectManager.get_associated_project_id(context)
2094 | if not project_id:
2095 | await context.send_messages(
2096 | NewConversationMessage(
2097 | content="You are not associated with a project.",
2098 | message_type=MessageType.notice,
2099 | )
2100 | )
2101 | return
2102 |
2103 | # Get all linked conversations
2104 | linked_conversation_ids = await ConversationProjectManager.get_linked_conversations(context)
2105 |
2106 | if not linked_conversation_ids:
2107 | await context.send_messages(
2108 | NewConversationMessage(
2109 | content="No linked conversations found. Invite participants with the `/invite` command.",
2110 | message_type=MessageType.notice,
2111 | )
2112 | )
2113 | return
2114 |
2115 | # Get participant information for all linked conversations
2116 | output = ["## Project Participants\n"]
2117 |
2118 | # First add information about this conversation
2119 | participants = await context.get_participants()
2120 |
2121 | output.append("### Coordinator Team\n")
2122 | for participant in participants.participants:
2123 | if participant.id != context.assistant.id:
2124 | output.append(f"- {participant.name}")
2125 |
2126 | # In the simplified implementation, we don't have detail about the linked conversations
2127 | # For a more complete implementation, we would need to get information
2128 | # about each linked conversation
2129 |
2130 | # For now, just report that we have no other team members
2131 | output.append("\n*No team members yet. Invite team members with the `/invite` command.*")
2132 |
2133 | # Send the information
2134 | await context.send_messages(
2135 | NewConversationMessage(
2136 | content="\n".join(output),
2137 | message_type=MessageType.chat,
2138 | )
2139 | )
2140 |
2141 | except Exception as e:
2142 | logger.exception(f"Error listing participants: {e}")
2143 | await context.send_messages(
2144 | NewConversationMessage(
2145 | content=f"Error listing participants: {str(e)}",
2146 | message_type=MessageType.notice,
2147 | )
2148 | )
2149 |
2150 |
2151 | # File synchronization command handler
2152 | async def handle_sync_files_command(
2153 | context: ConversationContext, message: ConversationMessage, args: List[str]
2154 | ) -> None:
2155 | """
2156 | Handle the sync-files command which synchronizes shared files from Coordinator to Team.
2157 |
2158 | This is primarily for Team members to explicitly request a file sync
2159 | if they suspect files are out of sync or missing.
2160 | """
2161 | try:
2162 | # Get project ID
2163 | project_id = await ProjectManager.get_project_id(context)
2164 | if not project_id:
2165 | await context.send_messages(
2166 | NewConversationMessage(
2167 | content="You are not associated with a project. Please join a project first.",
2168 | message_type=MessageType.notice,
2169 | )
2170 | )
2171 | return
2172 |
2173 | # Import the file manager
2174 | from .project_files import ProjectFileManager
2175 |
2176 | # Start sync with a simple message
2177 | await context.send_messages(
2178 | NewConversationMessage(
2179 | content="Synchronizing files from project...",
2180 | message_type=MessageType.notice,
2181 | )
2182 | )
2183 |
2184 | # Perform synchronization directly - this handles all error messaging
2185 | await ProjectFileManager.synchronize_files_to_team_conversation(context=context, project_id=project_id)
2186 |
2187 | except Exception as e:
2188 | logger.exception(f"Error synchronizing files: {e}")
2189 | await context.send_messages(
2190 | NewConversationMessage(
2191 | content=f"Error synchronizing files: {str(e)}",
2192 | message_type=MessageType.notice,
2193 | )
2194 | )
2195 |
2196 |
2197 | # General commands (available to all)
2198 | command_registry.register_command(
2199 | "help",
2200 | handle_help_command,
2201 | "Get help with available commands",
2202 | "/help [command]",
2203 | "/help project-info",
2204 | None, # Available to all roles
2205 | )
2206 |
2207 | command_registry.register_command(
2208 | "project-info",
2209 | handle_project_info_command,
2210 | "View project information",
2211 | "/project-info [brief|whiteboard|status|requests]",
2212 | "/project-info brief",
2213 | None, # Available to all roles
2214 | )
2215 |
2216 | # Team management commands
2217 | # Note: Manual project joining with /join is no longer needed - users just click the share URL
2218 |
2219 | command_registry.register_command(
2220 | "list-participants",
2221 | handle_list_participants_command,
2222 | "List all project participants",
2223 | "/list-participants",
2224 | "/list-participants",
2225 | ["coordinator"], # Only Coordinator can list participants
2226 | )
2227 |
2228 |
2229 | # Coordinator commands
2230 | command_registry.register_command(
2231 | "create-brief",
2232 | handle_create_brief_command,
2233 | "Create a brief",
2234 | "/create-brief Title|Description",
2235 | "/create-brief Website Redesign|We need to modernize our company website to improve user experience and conversions.",
2236 | ["coordinator"], # Only Coordinator can create briefs
2237 | )
2238 |
2239 | command_registry.register_command(
2240 | "add-goal",
2241 | handle_add_goal_command,
2242 | "Add a goal",
2243 | "/add-goal Goal Name|Goal description|Success criterion 1;Success criterion 2",
2244 | "/add-goal Redesign Homepage|Create a new responsive homepage|Design approved by stakeholders;Mobile compatibility verified",
2245 | ["coordinator"], # Only Coordinator can add goals
2246 | )
2247 |
2248 |
2249 | command_registry.register_command(
2250 | "resolve-request",
2251 | handle_resolve_request_command,
2252 | "Resolve an information request",
2253 | "/resolve-request request_id|Resolution information",
2254 | "/resolve-request abc123|The API documentation can be found at docs.example.com/api",
2255 | ["coordinator"], # Only Coordinator can resolve requests
2256 | )
2257 |
2258 | # Team commands
2259 | command_registry.register_command(
2260 | "request-info",
2261 | handle_request_info_command,
2262 | "Request information or assistance from the Coordinator",
2263 | "/request-info Request Title|Request description|priority",
2264 | "/request-info Need API Documentation|I need access to the API documentation for integration|high",
2265 | ["team"], # Only team can create requests
2266 | )
2267 |
2268 | command_registry.register_command(
2269 | "update-status",
2270 | handle_update_status_command,
2271 | "Update project status and progress",
2272 | "/update-status status|progress|message",
2273 | "/update-status in_progress|50|Completed homepage wireframes, working on mobile design",
2274 | ["team"], # Only team can update status
2275 | )
2276 |
2277 | # File synchronization command (primarily for team members)
2278 | command_registry.register_command(
2279 | "sync-files",
2280 | handle_sync_files_command,
2281 | "Synchronize shared files from the project to this conversation",
2282 | "/sync-files",
2283 | "/sync-files",
2284 | ["team"], # Primarily for team members
2285 | )
2286 |
2287 |
2288 | === File: assistants/project-assistant/assistant/config.py ===
2289 | from semantic_workbench_assistant.assistant_app import (
2290 | BaseModelAssistantConfig,
2291 | )
2292 |
2293 | from .configs import (
2294 | AssistantConfigModel,
2295 | CoordinatorConfig,
2296 | KnowledgeTransferConfigModel,
2297 | RequestConfig,
2298 | TeamConfig,
2299 | )
2300 |
2301 | assistant_config = BaseModelAssistantConfig(
2302 | AssistantConfigModel,
2303 | additional_templates={
2304 | "knowledge_transfer": KnowledgeTransferConfigModel,
2305 | },
2306 | )
2307 |
2308 | __all__ = [
2309 | "AssistantConfigModel",
2310 | "KnowledgeTransferConfigModel",
2311 | "CoordinatorConfig",
2312 | "RequestConfig",
2313 | "TeamConfig",
2314 | ]
2315 |
2316 |
2317 | === File: assistants/project-assistant/assistant/configs/__init__.py ===
2318 | from .default import AssistantConfigModel, CoordinatorConfig, RequestConfig, TeamConfig
2319 | from .knowledge_transfer import KnowledgeTransferConfigModel
2320 |
2321 | __all__ = [
2322 | "AssistantConfigModel",
2323 | "KnowledgeTransferConfigModel",
2324 | "CoordinatorConfig",
2325 | "RequestConfig",
2326 | "TeamConfig",
2327 | ]
2328 |
2329 |
2330 | === File: assistants/project-assistant/assistant/configs/default.py ===
2331 | from typing import Annotated
2332 |
2333 | import openai_client
2334 | from assistant_extensions.attachments import AttachmentsConfigModel
2335 | from content_safety.evaluators import CombinedContentSafetyEvaluatorConfig
2336 | from pydantic import BaseModel, ConfigDict, Field
2337 | from semantic_workbench_assistant.config import UISchema
2338 |
2339 | from ..utils import load_text_include
2340 |
2341 |
2342 | class RequestConfig(BaseModel):
2343 | model_config = ConfigDict(
2344 | title="Response Generation",
2345 | json_schema_extra={
2346 | "required": ["max_tokens", "response_tokens", "openai_model"],
2347 | },
2348 | )
2349 |
2350 | max_tokens: Annotated[
2351 | int,
2352 | Field(
2353 | title="Max Tokens",
2354 | description=(
2355 | "The maximum number of tokens to use for both the prompt and response. Current max supported by OpenAI"
2356 | " is 128k tokens, but varies by model (https://platform.openai.com/docs/models)"
2357 | ),
2358 | ),
2359 | ] = 64_000
2360 |
2361 | response_tokens: Annotated[
2362 | int,
2363 | Field(
2364 | title="Response Tokens",
2365 | description=(
2366 | "The number of tokens to use for the response, will reduce the number of tokens available for the"
2367 | " prompt. Current max supported by OpenAI is 4096 tokens (https://platform.openai.com/docs/models)"
2368 | ),
2369 | ),
2370 | ] = 8_000
2371 |
2372 | coordinator_conversation_token_limit: Annotated[
2373 | int,
2374 | Field(
2375 | title="Coordinator Conversation Token Limit",
2376 | description="The maximum number of tokens to use for the coordinator conversation history.",
2377 | ),
2378 | ] = 4000
2379 |
2380 | openai_model: Annotated[
2381 | str,
2382 | Field(title="OpenAI Model", description="The OpenAI model to use for generating responses."),
2383 | ] = "gpt-4o"
2384 |
2385 |
2386 | class PromptConfig(BaseModel):
2387 | model_config = ConfigDict(
2388 | title="Prompt Templates",
2389 | json_schema_extra={
2390 | "required": [
2391 | "coordinator_role",
2392 | "coordinator_instructions",
2393 | "team_role",
2394 | "team_instructions",
2395 | "whiteboard_prompt",
2396 | "project_information_request_detection",
2397 | ],
2398 | },
2399 | )
2400 |
2401 | coordinator_role: Annotated[
2402 | str,
2403 | Field(
2404 | title="Coordinator Role",
2405 | description="The role of the coordinator assistant. This is added to the prompt when in coordinator mode.",
2406 | ),
2407 | UISchema(widget="textarea"),
2408 | ] = load_text_include("coordinator_role.txt")
2409 |
2410 | coordinator_instructions: Annotated[
2411 | str,
2412 | Field(
2413 | title="Coordinator Instructions",
2414 | description="The instructions to give the coordinator assistant. This is added to the prompt when in coordinator mode.",
2415 | ),
2416 | UISchema(widget="textarea"),
2417 | ] = load_text_include("coordinator_instructions.txt")
2418 |
2419 | team_role: Annotated[
2420 | str,
2421 | Field(
2422 | title="Team Role",
2423 | description="The role of the team assistant. This is added to the prompt when in team member mode.",
2424 | ),
2425 | UISchema(widget="textarea"),
2426 | ] = load_text_include("team_role.txt")
2427 |
2428 | team_instructions: Annotated[
2429 | str,
2430 | Field(
2431 | title="Team Instructions",
2432 | description="The instructions to give the team assistant. This is added to the prompt when in team member mode.",
2433 | ),
2434 | UISchema(widget="textarea"),
2435 | ] = load_text_include("team_instructions.txt")
2436 |
2437 | project_information_request_detection: Annotated[
2438 | str,
2439 | Field(
2440 | title="Information Request Detection Prompt",
2441 | description="The prompt used to detect information requests in project assistant mode.",
2442 | ),
2443 | UISchema(widget="textarea"),
2444 | ] = load_text_include("project_information_request_detection.txt")
2445 |
2446 | whiteboard_prompt: Annotated[
2447 | str,
2448 | Field(title="Whiteboard Prompt", description="The prompt used to generate whiteboard content."),
2449 | UISchema(widget="textarea"),
2450 | ] = load_text_include("whiteboard_prompt.txt")
2451 |
2452 | welcome_message_generation: Annotated[
2453 | str,
2454 | Field(
2455 | title="Welcome Message generation prompt",
2456 | description="The prompt used to generate a welcome message for new team conversations.",
2457 | ),
2458 | UISchema(widget="textarea"),
2459 | ] = load_text_include("welcome_message_generation.txt")
2460 |
2461 |
2462 | class CoordinatorConfig(BaseModel):
2463 | model_config = ConfigDict(
2464 | title="Coordinator Configuration",
2465 | json_schema_extra={
2466 | "required": ["welcome_message", "prompt_for_files"],
2467 | },
2468 | )
2469 |
2470 | welcome_message: Annotated[
2471 | str,
2472 | Field(
2473 | title="Coordinator Welcome Message",
2474 | description="The message to display when a coordinator starts a new project. {share_url} will be replaced with the actual URL.",
2475 | ),
2476 | UISchema(widget="textarea"),
2477 | ] = """# Welcome to the Project Assistant
2478 |
2479 | This conversation is your personal conversation as the project coordinator.
2480 |
2481 | **To invite team members to your project, copy and share this link with them:**
2482 | [Join Team Conversation]({share_url})
2483 |
2484 | I've created a brief for your project. Let's start by updating it with your project goals and details."""
2485 |
2486 | prompt_for_files: Annotated[
2487 | str,
2488 | Field(
2489 | title="File Upload Prompt",
2490 | description="The message used to prompt project coordinators to upload relevant files.",
2491 | ),
2492 | UISchema(widget="textarea"),
2493 | ] = "To begin building your project context, would you like to upload any relevant files like documents, images, or data that your team will need? You can drag and drop files directly into this conversation."
2494 |
2495 | list_participants_command: Annotated[
2496 | str,
2497 | Field(
2498 | title="List Participants Command",
2499 | description="The command project coordinators can use to list all participants (without the slash).",
2500 | ),
2501 | ] = "list-participants"
2502 |
2503 |
2504 | class TeamConfig(BaseModel):
2505 | model_config = ConfigDict(
2506 | title="Team Member Configuration",
2507 | json_schema_extra={
2508 | "required": ["default_welcome_message", "status_command"],
2509 | },
2510 | )
2511 |
2512 | default_welcome_message: Annotated[
2513 | str,
2514 | Field(
2515 | title="Team Welcome Message",
2516 | description="The message to display when a user joins a project as a Team member. Shown after successfully joining a project.",
2517 | ),
2518 | UISchema(widget="textarea"),
2519 | ] = "# Welcome to Your Team Conversation\n\nYou've joined this project as a team member. This is your personal conversation for working on the project. You can communicate with the assistant, make information requests, and track your progress here."
2520 |
2521 | status_command: Annotated[
2522 | str,
2523 | Field(
2524 | title="Status Command",
2525 | description="The command project participants can use to check project status (without the slash).",
2526 | ),
2527 | ] = "project-status"
2528 |
2529 |
2530 | # Base Assistant Configuration - shared by all templates
2531 | class AssistantConfigModel(BaseModel):
2532 | project_or_context: Annotated[str, UISchema(widget="hidden")] = "project"
2533 | Project_or_Context: Annotated[str, UISchema(widget="hidden")] = "Project"
2534 | enable_debug_output: Annotated[
2535 | bool,
2536 | Field(
2537 | title="Include Debug Output",
2538 | description="Include debug output on conversation messages.",
2539 | ),
2540 | ] = False
2541 |
2542 | prompt_config: Annotated[
2543 | PromptConfig,
2544 | Field(
2545 | title="Prompt Configuration",
2546 | description="Configuration for prompt templates used throughout the assistant.",
2547 | ),
2548 | ] = PromptConfig()
2549 |
2550 | request_config: Annotated[
2551 | RequestConfig,
2552 | Field(
2553 | title="Request Configuration",
2554 | ),
2555 | ] = RequestConfig()
2556 |
2557 | service_config: openai_client.ServiceConfig
2558 |
2559 | content_safety_config: Annotated[
2560 | CombinedContentSafetyEvaluatorConfig,
2561 | Field(
2562 | title="Content Safety Configuration",
2563 | ),
2564 | ] = CombinedContentSafetyEvaluatorConfig()
2565 |
2566 | attachments_config: Annotated[
2567 | AttachmentsConfigModel,
2568 | Field(
2569 | title="Attachments Configuration",
2570 | description="Configuration for handling file attachments in messages.",
2571 | ),
2572 | ] = AttachmentsConfigModel()
2573 |
2574 | # Project configuration fields moved directly into AssistantConfigModel
2575 | auto_sync_files: Annotated[
2576 | bool,
2577 | Field(
2578 | title="Auto-sync Files",
2579 | description="Automatically synchronize files between linked conversations.",
2580 | ),
2581 | ] = True
2582 |
2583 | track_progress: Annotated[
2584 | bool,
2585 | Field(
2586 | title="Track Progress",
2587 | description="Track project progress with goals, criteria completion, and overall project state.",
2588 | ),
2589 | ] = True
2590 |
2591 | proactive_guidance: Annotated[
2592 | bool,
2593 | Field(
2594 | title="Proactive Guidance",
2595 | description="Proactively guide project coordinators through context building.",
2596 | ),
2597 | ] = True
2598 |
2599 | coordinator_config: Annotated[
2600 | CoordinatorConfig,
2601 | Field(
2602 | title="Coordinator Configuration",
2603 | description="Configuration for project coordinators.",
2604 | ),
2605 | ] = CoordinatorConfig()
2606 |
2607 | team_config: Annotated[
2608 | TeamConfig,
2609 | Field(
2610 | title="Team Configuration",
2611 | description="Configuration for project team members.",
2612 | ),
2613 | ] = TeamConfig()
2614 |
2615 |
2616 | === File: assistants/project-assistant/assistant/configs/knowledge_transfer.py ===
2617 | from typing import Annotated
2618 |
2619 | from pydantic import Field
2620 | from semantic_workbench_assistant.config import UISchema
2621 |
2622 | from ..utils import load_text_include
2623 | from .default import AssistantConfigModel, CoordinatorConfig, PromptConfig, TeamConfig
2624 |
2625 |
2626 | class KnowledgeTransferPromptConfig(PromptConfig):
2627 | """Prompt configuration specific to knowledge transfer template."""
2628 |
2629 | whiteboard_prompt: Annotated[
2630 | str,
2631 | Field(
2632 | title="Knowledge Transfer Whiteboard Prompt",
2633 | description="The prompt used to generate whiteboard content in knowledge transfer mode.",
2634 | ),
2635 | ] = load_text_include("knowledge_transfer_whiteboard_prompt.txt")
2636 |
2637 | project_information_request_detection: Annotated[
2638 | str,
2639 | Field(
2640 | title="Knowledge Transfer Information Request Detection Prompt",
2641 | description="The prompt used to detect information requests in knowledge transfer mode.",
2642 | ),
2643 | ] = load_text_include("knowledge_transfer_information_request_detection.txt")
2644 |
2645 | welcome_message_generation: Annotated[
2646 | str,
2647 | Field(
2648 | title="Welcome Message generation prompt",
2649 | description="The prompt used to generate a welcome message for new team conversations.",
2650 | ),
2651 | UISchema(widget="textarea"),
2652 | ] = load_text_include("knowledge_transfer_welcome_message_generation.txt")
2653 |
2654 |
2655 | class KnowledgeTransferCoordinatorConfig(CoordinatorConfig):
2656 | """Coordinator configuration specific to knowledge transfer template."""
2657 |
2658 | welcome_message: Annotated[
2659 | str,
2660 | Field(
2661 | title="Knowledge Transfer Coordinator Welcome Message",
2662 | description="The message to display when a coordinator starts a new knowledge transfer project. {share_url} will be replaced with the actual URL.",
2663 | ),
2664 | ] = """# Welcome to Knowledge Transfer
2665 |
2666 | Welcome! I'm here to help you capture and share complex information in a way that others can easily explore and understand. Think of me as your personal knowledge bridge - I'll help you:
2667 |
2668 | - 📚 Organize your thoughts - whether from documents, code, research papers, or brainstorming sessions
2669 | - 🔄 Establish shared understanding - I'll ask questions to ensure we're aligned on what matters most
2670 | - 🔍 Make your knowledge interactive - so others can explore the "why" behind decisions, alternatives considered, and deeper context
2671 | - 🔗 Create shareable experiences - I'll capture what knowledge you give me so it can be shared with your team members for them to explore at their own pace using this [Knowledge Transfer link]({share_url})
2672 |
2673 | Simply share your content or ideas, tell me who needs to understand them, and what aspects you want to highlight. I'll capture what knowledge you give me so it can be shared with your team members for them to explore at their own pace.
2674 |
2675 | In the side panel, you can see your "knowledge brief". This brief will be shared with your team members and will help them understand the content of your knowledge transfer. You can ask me to update it at any time.
2676 |
2677 | What knowledge would you like to transfer today?"""
2678 |
2679 |
2680 | class KnowledgeTransferTeamConfig(TeamConfig):
2681 | """Team configuration specific to knowlege transfer template."""
2682 |
2683 | default_welcome_message: Annotated[
2684 | str,
2685 | Field(
2686 | title="Knowledge Transfer Team Welcome Message",
2687 | description="The message to display when a user joins as a Team member in knowledge transfer mode.",
2688 | ),
2689 | ] = "# Welcome to your Knowledge Transfer space!\n\nYou now have access to the shared knowledge that has been prepared for you. This is your personal conversation for exploring your knowledge space."
2690 |
2691 |
2692 | class KnowledgeTransferConfigModel(AssistantConfigModel):
2693 | project_or_context: Annotated[str, UISchema(widget="hidden")] = "knowledge"
2694 | Project_or_Context: Annotated[str, UISchema(widget="hidden")] = "Knowledge"
2695 |
2696 | prompt_config: Annotated[
2697 | PromptConfig,
2698 | Field(
2699 | title="Prompt Configuration",
2700 | description="Configuration for prompt templates used throughout the assistant.",
2701 | ),
2702 | ] = KnowledgeTransferPromptConfig()
2703 |
2704 | proactive_guidance: Annotated[
2705 | bool,
2706 | Field(
2707 | title="Proactive Guidance",
2708 | description="Proactively guide knowledge organizers through knowledge structuring.",
2709 | ),
2710 | ] = True
2711 |
2712 | track_progress: Annotated[
2713 | bool,
2714 | Field(
2715 | title="Track Progress",
2716 | description="Track project progress with goals, criteria completion, and overall project state.",
2717 | ),
2718 | ] = False
2719 |
2720 | coordinator_config: Annotated[
2721 | CoordinatorConfig,
2722 | Field(
2723 | title="Knowledge Transfer Coordinator Configuration",
2724 | description="Configuration for coordinators in knowledge transfer mode.",
2725 | ),
2726 | ] = KnowledgeTransferCoordinatorConfig()
2727 |
2728 | team_config: Annotated[
2729 | TeamConfig,
2730 | Field(
2731 | title="Knowledge Transfer Team Configuration",
2732 | description="Configuration for team members in knowledge transfer mode.",
2733 | ),
2734 | ] = KnowledgeTransferTeamConfig()
2735 |
2736 |
2737 | === File: assistants/project-assistant/assistant/conversation_clients.py ===
2738 | """
2739 | Project assistant functionality for cross-conversation communication.
2740 |
2741 | This module handles the project assistant's core functionality for managing
2742 | communication between conversations. It provides utilities for creating temporary
2743 | contexts and accessing other conversations.
2744 | """
2745 |
2746 | from typing import Any, Optional, Tuple
2747 |
2748 | from semantic_workbench_api_model.workbench_service_client import ConversationAPIClient
2749 | from semantic_workbench_assistant.assistant_app import ConversationContext
2750 | from semantic_workbench_assistant.storage import read_model
2751 |
2752 | from .conversation_project_link import ConversationProjectManager
2753 | from .logging import logger
2754 | from .project_storage import ProjectStorageManager
2755 | from .project_storage_models import ConversationRole
2756 |
2757 |
2758 | class ConversationClientManager:
2759 | """
2760 | Manages API clients for accessing other conversations.
2761 |
2762 | This utility class provides methods for creating API clients and temporary contexts
2763 | that can be used to interact with other conversations in the same project.
2764 | """
2765 |
2766 | @staticmethod
2767 | def get_conversation_client(context: ConversationContext, conversation_id: str) -> ConversationAPIClient:
2768 | """
2769 | Gets a client for accessing another conversation.
2770 | """
2771 | return context.for_conversation(conversation_id)._conversation_client
2772 |
2773 | @staticmethod
2774 | async def get_coordinator_client_for_project(
2775 | context: ConversationContext, project_id: str
2776 | ) -> Tuple[Optional[Any], Optional[str]]:
2777 | """
2778 | Gets a client for accessing the Coordinator conversation for a project.
2779 | """
2780 | # Look for the Coordinator conversation directory
2781 | coordinator_dir = ProjectStorageManager.get_project_dir(project_id) / ConversationRole.COORDINATOR
2782 | if not coordinator_dir.exists():
2783 | return None, None
2784 |
2785 | # Find the role file that contains the conversation ID
2786 | role_file = coordinator_dir / "conversation_role.json"
2787 | if not role_file.exists():
2788 | role_file = coordinator_dir / "project_role.json"
2789 | if not role_file.exists():
2790 | return None, None
2791 |
2792 | # Read the role information to get the Coordinator conversation ID
2793 | role_data = read_model(role_file, ConversationProjectManager.ConversationRoleInfo)
2794 | if not role_data or not role_data.conversation_id:
2795 | return None, None
2796 |
2797 | # Get the Coordinator conversation ID
2798 | coordinator_conversation_id = role_data.conversation_id
2799 |
2800 | # Don't create a client if the Coordinator is the current conversation
2801 | if coordinator_conversation_id == str(context.id):
2802 | return None, coordinator_conversation_id
2803 |
2804 | # Create a client for the Coordinator conversation
2805 | client = ConversationClientManager.get_conversation_client(context, coordinator_conversation_id)
2806 | return client, coordinator_conversation_id
2807 |
2808 | @staticmethod
2809 | async def create_temporary_context_for_conversation(
2810 | source_context: ConversationContext, target_conversation_id: str
2811 | ) -> Optional[ConversationContext]:
2812 | """
2813 | Creates a temporary context for the target conversation ID.
2814 | """
2815 | try:
2816 | # Create a temporary context with the same properties as the original
2817 | # but pointing to a different conversation
2818 |
2819 | temp_context = source_context.for_conversation(target_conversation_id)
2820 |
2821 | return temp_context
2822 |
2823 | except Exception as e:
2824 | logger.error(f"Error creating temporary context: {e}")
2825 | return None
2826 |
2827 |
2828 | === File: assistants/project-assistant/assistant/conversation_project_link.py ===
2829 | """
2830 | Manages associations between conversations and projects.
2831 |
2832 | This module handles the linking of conversations to projects,
2833 | defining roles (Coordinator/Team) and maintaining relationships.
2834 | """
2835 |
2836 | from typing import List, Optional
2837 |
2838 | from pydantic import BaseModel
2839 | from semantic_workbench_assistant.assistant_app import ConversationContext
2840 | from semantic_workbench_assistant.storage import read_model, write_model
2841 |
2842 | from .logging import logger
2843 | from .project_storage import ProjectStorageManager
2844 | from .project_storage_models import ConversationRole
2845 |
2846 |
2847 | class ConversationProjectManager:
2848 | """Manages the association between conversations and projects."""
2849 |
2850 | class ConversationRoleInfo(BaseModel):
2851 | """Stores a conversation's role in a project."""
2852 |
2853 | project_id: str
2854 | role: ConversationRole
2855 | conversation_id: str
2856 |
2857 | class ProjectAssociation(BaseModel):
2858 | """Stores a conversation's project association."""
2859 |
2860 | project_id: str
2861 |
2862 | @staticmethod
2863 | async def get_linked_conversations(context: ConversationContext) -> List[str]:
2864 | """
2865 | Gets all conversations linked to this one through the same project.
2866 | """
2867 | try:
2868 | # Get project ID
2869 | project_id = await ConversationProjectManager.get_associated_project_id(context)
2870 | if not project_id:
2871 | return []
2872 |
2873 | # Get the linked conversations directory
2874 | linked_dir = ProjectStorageManager.get_linked_conversations_dir(project_id)
2875 | if not linked_dir.exists():
2876 | return []
2877 |
2878 | # Get all conversation files in the directory
2879 | result = []
2880 | conversation_id = str(context.id)
2881 |
2882 | # Each file in the directory represents a linked conversation
2883 | # The filename itself is the conversation ID
2884 | for file_path in linked_dir.glob("*"):
2885 | if file_path.is_file():
2886 | # The filename is the conversation ID
2887 | conv_id = file_path.name
2888 | if conv_id != conversation_id:
2889 | result.append(conv_id)
2890 |
2891 | return result
2892 |
2893 | except Exception as e:
2894 | logger.error(f"Error getting linked conversations: {e}")
2895 | return []
2896 |
2897 | @staticmethod
2898 | async def set_conversation_role(context: ConversationContext, project_id: str, role: ConversationRole) -> None:
2899 | """
2900 | Sets the role of a conversation in a project.
2901 | """
2902 | role_data = ConversationProjectManager.ConversationRoleInfo(
2903 | project_id=project_id, role=role, conversation_id=str(context.id)
2904 | )
2905 | role_path = ProjectStorageManager.get_conversation_role_file_path(context)
2906 | write_model(role_path, role_data)
2907 |
2908 | @staticmethod
2909 | async def get_conversation_role(context: ConversationContext) -> Optional[ConversationRole]:
2910 | """
2911 | Gets the role of a conversation in a project.
2912 | """
2913 | role_path = ProjectStorageManager.get_conversation_role_file_path(context)
2914 | role_data = read_model(role_path, ConversationProjectManager.ConversationRoleInfo)
2915 |
2916 | if role_data:
2917 | return role_data.role
2918 |
2919 | return None
2920 |
2921 | @staticmethod
2922 | async def associate_conversation_with_project(context: ConversationContext, project_id: str) -> None:
2923 | """
2924 | Associates a conversation with a project.
2925 | """
2926 | logger.debug(f"Associating conversation {context.id} with project {project_id}")
2927 |
2928 | try:
2929 | # 1. Store the project association in the conversation's storage directory
2930 | project_data = ConversationProjectManager.ProjectAssociation(project_id=project_id)
2931 | project_path = ProjectStorageManager.get_conversation_project_file_path(context)
2932 | logger.debug(f"Writing project association to {project_path}")
2933 | write_model(project_path, project_data)
2934 |
2935 | # 2. Register this conversation in the project's linked_conversations directory
2936 | linked_dir = ProjectStorageManager.get_linked_conversations_dir(project_id)
2937 | logger.debug(f"Registering in linked_conversations directory: {linked_dir}")
2938 | conversation_file = linked_dir / str(context.id)
2939 |
2940 | # Touch the file to create it if it doesn't exist
2941 | # We don't need to write any content to it, just its existence is sufficient
2942 | conversation_file.touch(exist_ok=True)
2943 | logger.debug(f"Created conversation link file: {conversation_file}")
2944 | except Exception as e:
2945 | logger.error(f"Error associating conversation with project: {e}")
2946 | raise
2947 |
2948 | @staticmethod
2949 | async def get_associated_project_id(context: ConversationContext) -> Optional[str]:
2950 | """
2951 | Gets the project ID associated with a conversation.
2952 | """
2953 | project_path = ProjectStorageManager.get_conversation_project_file_path(context)
2954 | project_data = read_model(project_path, ConversationProjectManager.ProjectAssociation)
2955 |
2956 | if project_data:
2957 | return project_data.project_id
2958 |
2959 | return None
2960 |
2961 |
2962 | === File: assistants/project-assistant/assistant/logging.py ===
2963 | """
2964 | Logging utilities for project assistant.
2965 |
2966 | This module provides enhanced logging capabilities for the project assistant,
2967 | including JSON formatting and file logging.
2968 | """
2969 |
2970 | import json
2971 | import logging
2972 | import os
2973 | import platform
2974 | from datetime import datetime
2975 | from pathlib import Path
2976 | from typing import Any, Dict, Optional
2977 | from uuid import UUID
2978 |
2979 | from pydantic import BaseModel
2980 |
2981 | # Configure the root logger
2982 | logger = logging.getLogger("project-assistant")
2983 | logger.setLevel(logging.DEBUG)
2984 |
2985 | # Ensure propagation is enabled to allow logs to reach the root handler
2986 | logger.propagate = True
2987 |
2988 | # Remove any existing handlers to avoid duplicates
2989 | for handler in logger.handlers[:]:
2990 | logger.removeHandler(handler)
2991 |
2992 | # Add a null handler by default (to prevent "No handler found" warnings)
2993 | logger.addHandler(logging.NullHandler())
2994 |
2995 | # Set up console handler with a specific format
2996 | console = logging.StreamHandler()
2997 | console.setLevel(logging.INFO)
2998 | formatter = logging.Formatter("%(asctime)s - %(name)s - [%(levelname)s] %(message)s")
2999 | console.setFormatter(formatter)
3000 | logger.addHandler(console)
3001 |
3002 |
3003 | def convert_to_serializable(data: Any) -> Any:
3004 | """
3005 | Recursively convert Pydantic BaseModel instances to dictionaries.
3006 | """
3007 | if isinstance(data, BaseModel):
3008 | return data.model_dump()
3009 | elif isinstance(data, dict):
3010 | return {key: convert_to_serializable(value) for key, value in data.items()}
3011 | elif isinstance(data, list):
3012 | return [convert_to_serializable(item) for item in data]
3013 | elif isinstance(data, tuple):
3014 | return tuple(convert_to_serializable(item) for item in data)
3015 | elif isinstance(data, set):
3016 | return {convert_to_serializable(item) for item in data}
3017 | return data
3018 |
3019 |
3020 | class CustomEncoder(json.JSONEncoder):
3021 | """Custom JSON encoder that handles UUIDs and datetimes."""
3022 |
3023 | def default(self, o: Any) -> Any:
3024 | if isinstance(o, UUID):
3025 | return str(o)
3026 | if isinstance(o, datetime):
3027 | return o.isoformat()
3028 | return super().default(o)
3029 |
3030 |
3031 | class JsonFormatter(logging.Formatter):
3032 | """Formats log records as JSON objects."""
3033 |
3034 | def format(self, record) -> str:
3035 | record_dict = record.__dict__
3036 | log_record = {
3037 | "timestamp": self.formatTime(record, self.datefmt),
3038 | "level": record.levelname,
3039 | "conversation_id": record_dict.get("conversation_id", None),
3040 | "project_id": record_dict.get("project_id", None),
3041 | "message": record.getMessage(),
3042 | "data": record_dict.get("data", None),
3043 | "module": record.module,
3044 | "functionName": record.funcName,
3045 | "lineNumber": record.lineno,
3046 | "logger": record.name,
3047 | }
3048 |
3049 | # Add any extra fields
3050 | extra_fields = {
3051 | key: value
3052 | for key, value in record.__dict__.items()
3053 | if key
3054 | not in [
3055 | "levelname",
3056 | "msg",
3057 | "args",
3058 | "funcName",
3059 | "module",
3060 | "lineno",
3061 | "name",
3062 | "message",
3063 | "asctime",
3064 | "conversation_id",
3065 | "project_id",
3066 | "data",
3067 | ]
3068 | }
3069 | log_record.update(extra_fields)
3070 |
3071 | try:
3072 | return json.dumps(log_record, cls=CustomEncoder)
3073 | except Exception as e:
3074 | # Fallback if serialization fails
3075 | simple_record = {
3076 | "timestamp": self.formatTime(record, self.datefmt),
3077 | "level": record.levelname,
3078 | "conversation_id": record_dict.get("conversation_id", None),
3079 | "message": f"Error serializing log: {e}. Original message: {record.getMessage()}",
3080 | "module": record.module,
3081 | "lineNumber": record.lineno,
3082 | }
3083 | return json.dumps(simple_record)
3084 |
3085 |
3086 | def setup_file_logging(log_dir: Optional[str] = None) -> Path:
3087 | """
3088 | Set up file logging with JSON formatting.
3089 |
3090 | Args:
3091 | log_dir: Directory for log files. If None, uses the project's .data/logs/ directory
3092 |
3093 | Returns:
3094 | Path to the log file
3095 | """
3096 | # By default, store logs in the project's .data directory
3097 | if log_dir is None:
3098 | # Get the directory where the current module is located
3099 | current_file = Path(__file__)
3100 | project_dir = current_file.parent.parent # Go up to project-assistant directory
3101 | log_path = project_dir / ".data" / "logs"
3102 | else:
3103 | # Use the provided directory
3104 | log_path = Path(log_dir)
3105 |
3106 | # Ensure directory exists
3107 | log_path.mkdir(parents=True, exist_ok=True)
3108 |
3109 | # Create log file path with timestamp to avoid conflicts
3110 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
3111 | log_file = log_path / f"project_assistant_{timestamp}.json"
3112 | line_log_file = log_path / f"project_assistant_{timestamp}.log" # Add a regular log file too
3113 |
3114 | try:
3115 | # Remove any existing file handlers to avoid duplicates
3116 | for handler in logger.handlers[:]:
3117 | if isinstance(handler, logging.FileHandler):
3118 | logger.removeHandler(handler)
3119 |
3120 | # Set up JSON file handler
3121 | json_file_handler = logging.FileHandler(log_file)
3122 | json_file_handler.setLevel(logging.DEBUG)
3123 | json_file_handler.setFormatter(JsonFormatter())
3124 | logger.addHandler(json_file_handler)
3125 |
3126 | # Also set up a regular text file handler for easier debugging
3127 | text_file_handler = logging.FileHandler(line_log_file)
3128 | text_file_handler.setLevel(logging.DEBUG)
3129 | text_file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - [%(levelname)s] %(message)s"))
3130 | logger.addHandler(text_file_handler)
3131 |
3132 | logger.debug(
3133 | f"File logging enabled: {log_file}",
3134 | extra={
3135 | "system": platform.system(),
3136 | "python_version": platform.python_version(),
3137 | "app": "project-assistant",
3138 | "path": str(log_file.absolute()),
3139 | },
3140 | )
3141 |
3142 | # Also force a flush to ensure the log is written immediately
3143 | for handler in logger.handlers:
3144 | if hasattr(handler, "flush"):
3145 | handler.flush()
3146 |
3147 | # Set permissions to ensure files are readable (for debugging)
3148 | try:
3149 | import stat
3150 |
3151 | os.chmod(log_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
3152 | os.chmod(line_log_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
3153 | except Exception as e:
3154 | logger.warning(f"Could not set log file permissions: {e}")
3155 | print(f"Permission error: {e}")
3156 |
3157 | print(f"Logging to files: {log_file} and {line_log_file}")
3158 |
3159 | except Exception as e:
3160 | print(f"Failed to set up file logging: {e}")
3161 | # Fall back to a different location in the .data directory
3162 | try:
3163 | # Try a different subfolder in the .data directory
3164 | current_file = Path(__file__)
3165 | project_dir = current_file.parent.parent # Go up to project-assistant directory
3166 | fallback_dir = project_dir / ".data" / "fallback_logs"
3167 | os.makedirs(fallback_dir, exist_ok=True)
3168 | log_file = Path(fallback_dir) / f"project_assistant_{timestamp}.json"
3169 | line_log_file = Path(fallback_dir) / f"project_assistant_{timestamp}.log"
3170 |
3171 | json_file_handler = logging.FileHandler(log_file)
3172 | json_file_handler.setLevel(logging.DEBUG)
3173 | json_file_handler.setFormatter(JsonFormatter())
3174 | logger.addHandler(json_file_handler)
3175 |
3176 | text_file_handler = logging.FileHandler(line_log_file)
3177 | text_file_handler.setLevel(logging.DEBUG)
3178 | text_file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - [%(levelname)s] %(message)s"))
3179 | logger.addHandler(text_file_handler)
3180 |
3181 | logger.warning(f"Using fallback log location: {log_file}")
3182 | print(f"Fallback logging to: {log_file} and {line_log_file}")
3183 |
3184 | except Exception as fallback_error:
3185 | print(f"Failed to set up fallback logging: {fallback_error}")
3186 |
3187 | return log_file
3188 |
3189 |
3190 | def extra_data(data: Any) -> Dict[str, Any]:
3191 | """
3192 | Helper function to prepare extra data for log messages.
3193 |
3194 | Args:
3195 | data: Data to include in log entry
3196 |
3197 | Returns:
3198 | Dictionary with 'data' key containing serialized data
3199 | """
3200 | extra = {}
3201 |
3202 | # Convert to serializable format
3203 | data = convert_to_serializable(data)
3204 |
3205 | # Ensure data is JSON-serializable
3206 | try:
3207 | data = json.loads(json.dumps(data, cls=CustomEncoder))
3208 | except Exception as e:
3209 | data = str(e)
3210 |
3211 | if data:
3212 | extra["data"] = data
3213 |
3214 | return extra
3215 |
3216 |
3217 | # Make extra_data available for import
3218 | __all__ = ["setup_file_logging", "extra_data", "logger"]
3219 |
3220 |
3221 | === File: assistants/project-assistant/assistant/project_analysis.py ===
3222 | """
3223 | Analysis and detection functions for the project assistant.
3224 |
3225 | This module contains functions for analyzing messages and project content
3226 | to detect specific conditions, such as information request needs.
3227 | """
3228 |
3229 | import json
3230 | from typing import Any, Dict, List
3231 |
3232 | import openai_client
3233 | from openai.types.chat import ChatCompletionMessageParam
3234 | from semantic_workbench_assistant.assistant_app import ConversationContext
3235 |
3236 | from .config import assistant_config
3237 | from .logging import logger
3238 |
3239 |
3240 | async def detect_information_request_needs(context: ConversationContext, message: str) -> Dict[str, Any]:
3241 | """
3242 | Analyze a user message in context of recent chat history to detect potential information request needs.
3243 | Uses an LLM for sophisticated detection.
3244 |
3245 | Args:
3246 | context: The conversation context
3247 | message: The user message to analyze
3248 |
3249 | Returns:
3250 | Dict with detection results including is_information_request, confidence, and other metadata
3251 | """
3252 | debug: Dict[str, Any] = {
3253 | "message": message,
3254 | "context": context,
3255 | }
3256 |
3257 | # Get config via assistant config
3258 | config = await assistant_config.get(context.assistant)
3259 |
3260 | # Check if we're in a test environment (Missing parts of context)
3261 | if not hasattr(context, "assistant") or context.assistant is None:
3262 | return {
3263 | "is_information_request": False,
3264 | "reason": "Unable to perform detection in test environment - missing context",
3265 | "confidence": 0.0,
3266 | "debug": debug,
3267 | }
3268 |
3269 | # Get the config
3270 | config = await assistant_config.get(context.assistant)
3271 |
3272 | # Verify service_config is available
3273 | if not config.service_config:
3274 | logger.warning("No service_config available for LLM-based detection")
3275 | return {
3276 | "is_information_request": False,
3277 | "reason": "LLM detection unavailable - missing service configuration",
3278 | "confidence": 0.0,
3279 | "debug": debug,
3280 | }
3281 |
3282 | # Get recent conversation history (up to 10 messages)
3283 | chat_history = []
3284 | try:
3285 | # Get recent messages to provide context
3286 | messages_response = await context.get_messages(limit=10)
3287 | if messages_response and messages_response.messages:
3288 | # Format messages for the LLM
3289 | for msg in messages_response.messages:
3290 | # Format the sender name
3291 | sender_name = "Team Member"
3292 | if msg.sender.participant_id == context.assistant.id:
3293 | sender_name = "Assistant"
3294 |
3295 | # Add to chat history
3296 | role = "user" if sender_name == "Team Member" else "assistant"
3297 | chat_history.append({"role": role, "content": f"{sender_name}: {msg.content}"})
3298 |
3299 | # Reverse to get chronological order
3300 | chat_history.reverse()
3301 | except Exception as e:
3302 | logger.warning(f"Could not retrieve chat history: {e}")
3303 | # Continue without history if we can't get it
3304 |
3305 | try:
3306 | # Create chat completion with history context
3307 | async with openai_client.create_client(config.service_config) as client:
3308 | # Prepare messages array with system prompt and chat history
3309 | messages: List[ChatCompletionMessageParam] = [
3310 | {"role": "system", "content": config.prompt_config.project_information_request_detection}
3311 | ]
3312 |
3313 | # Add chat history if available
3314 | if chat_history:
3315 | for history_msg in chat_history:
3316 | messages.append({"role": history_msg["role"], "content": history_msg["content"]})
3317 |
3318 | # Add the current message for analysis - explicitly mark as the latest message
3319 | messages.append({"role": "user", "content": f"Latest message from Team Member: {message}"})
3320 |
3321 | completion_args = {
3322 | "model": "gpt-3.5-turbo",
3323 | "messages": messages,
3324 | "response_format": {"type": "json_object"},
3325 | "max_tokens": 500,
3326 | "temperature": 0.2, # Low temperature for more consistent analysis
3327 | }
3328 | debug["completion_args"] = openai_client.make_completion_args_serializable(completion_args)
3329 |
3330 | # Make the API call
3331 | response = await client.chat.completions.create(
3332 | **completion_args,
3333 | )
3334 | debug["completion_response"] = response.model_dump()
3335 |
3336 | # Extract and parse the response
3337 | if response and response.choices and response.choices[0].message.content:
3338 | try:
3339 | result = json.loads(response.choices[0].message.content)
3340 | # Add the original message for reference
3341 | result["original_message"] = message
3342 | return result
3343 | except json.JSONDecodeError:
3344 | logger.warning(f"Failed to parse JSON from LLM response: {response.choices[0].message.content}")
3345 | return {
3346 | "is_information_request": False,
3347 | "reason": "Failed to parse LLM response",
3348 | "confidence": 0.0,
3349 | }
3350 | else:
3351 | logger.warning("Empty response from LLM for information request detection")
3352 | return {
3353 | "is_information_request": False,
3354 | "reason": "Empty response from LLM",
3355 | "confidence": 0.0,
3356 | "debug": debug,
3357 | }
3358 | except Exception as e:
3359 | logger.exception(f"Error in LLM-based information request detection: {e}")
3360 | debug["error"] = str(e)
3361 | return {
3362 | "is_information_request": False,
3363 | "reason": f"LLM detection error: {str(e)}",
3364 | "confidence": 0.0,
3365 | "debug": debug,
3366 | }
3367 |
3368 |
3369 | === File: assistants/project-assistant/assistant/project_common.py ===
3370 | """
3371 | Common utilities shared between project assistant modes.
3372 |
3373 | This module provides shared functionality for team and coordinator conversation handlers,
3374 | helping to reduce code duplication and maintain consistency.
3375 | """
3376 |
3377 | from typing import Dict, Optional
3378 |
3379 | from semantic_workbench_assistant.assistant_app import ConversationContext
3380 |
3381 | from .conversation_project_link import ConversationProjectManager
3382 | from .logging import logger
3383 | from .project_data import LogEntryType
3384 | from .project_storage import ProjectStorage
3385 | from .project_storage_models import ConversationRole
3386 |
3387 |
3388 | async def detect_assistant_role(context: ConversationContext) -> ConversationRole:
3389 | """
3390 | Detects whether this conversation is in Coordinator or Team mode.
3391 |
3392 | This method examines the conversation metadata to determine the role
3393 | of the current conversation in the project. The role is always stored
3394 | in the conversation metadata as "project_role".
3395 |
3396 | Args:
3397 | context: The conversation context to examine
3398 |
3399 | Returns:
3400 | ConversationRole.COORDINATOR or ConversationRole.TEAM
3401 | """
3402 | try:
3403 | conversation = await context.get_conversation()
3404 | metadata = conversation.metadata or {}
3405 | role_str = metadata.get("project_role", "coordinator")
3406 |
3407 | if role_str == "team":
3408 | return ConversationRole.TEAM
3409 | else:
3410 | return ConversationRole.COORDINATOR
3411 | except Exception as e:
3412 | logger.exception(f"Error detecting assistant role: {e}")
3413 | # Default to coordinator role if we can't determine
3414 | return ConversationRole.COORDINATOR
3415 |
3416 |
3417 | async def log_project_action(
3418 | context: ConversationContext,
3419 | entry_type: LogEntryType,
3420 | message: str,
3421 | related_entity_id: Optional[str] = None,
3422 | additional_metadata: Optional[Dict] = None,
3423 | ) -> None:
3424 | """
3425 | Log an action to the project log.
3426 |
3427 | This utility function handles retrieving the project ID and logging the event
3428 | using the appropriate storage mechanism. It's used by both Coordinator and Team mode
3429 | handlers to maintain consistent logging.
3430 |
3431 | Args:
3432 | context: The conversation context
3433 | entry_type: Type of log entry
3434 | message: Human-readable description of the action
3435 | related_entity_id: Optional ID of a related entity (e.g., request ID)
3436 | additional_metadata: Optional additional metadata to include in the log
3437 | """
3438 | project_id = await ConversationProjectManager.get_associated_project_id(context)
3439 | if not project_id:
3440 | return
3441 |
3442 | await ProjectStorage.log_project_event(
3443 | context=context,
3444 | project_id=project_id,
3445 | entry_type=entry_type.value,
3446 | message=message,
3447 | related_entity_id=related_entity_id,
3448 | metadata=additional_metadata,
3449 | )
3450 |
3451 |
3452 | === File: assistants/project-assistant/assistant/project_data.py ===
3453 | """
3454 | Data models for project entities (briefs, information requests, logs, etc.)
3455 |
3456 | This module provides the core data structures for the project assistant,
3457 | without any artifact abstraction or unnecessary complexity.
3458 | """
3459 |
3460 | import uuid
3461 | from datetime import datetime
3462 | from enum import Enum
3463 | from typing import Any, Dict, List, Optional
3464 |
3465 | from pydantic import BaseModel, Field
3466 |
3467 |
3468 | class ProjectState(str, Enum):
3469 | """
3470 | States for project progression.
3471 |
3472 | The project state represents the current phase of the project lifecycle.
3473 | Projects follow a standard flow: PLANNING -> READY_FOR_WORKING -> IN_PROGRESS -> COMPLETED.
3474 | ABORTED is a terminal state that can be reached from any other state if the project is canceled.
3475 | """
3476 |
3477 | PLANNING = "planning" # Initial state - Coordinator is defining the project brief and goals
3478 | READY_FOR_WORKING = "ready_for_working" # Project is defined and ready for team members to begin work
3479 | IN_PROGRESS = "in_progress" # Team members are actively working on the project
3480 | COMPLETED = "completed" # Project goals have been achieved and the project is complete
3481 | ABORTED = "aborted" # Project was terminated early or canceled
3482 |
3483 |
3484 | class RequestPriority(str, Enum):
3485 | """
3486 | Priority levels for information requests.
3487 |
3488 | Defines the urgency of a request from a team member to the Coordinator.
3489 | Higher priority requests should be addressed more quickly by the Coordinator.
3490 | MEDIUM is the default priority for most requests.
3491 | """
3492 |
3493 | LOW = "low" # Non-urgent request, can be addressed when convenient
3494 | MEDIUM = "medium" # Standard priority for most requests
3495 | HIGH = "high" # Urgent request requiring prompt attention
3496 | CRITICAL = "critical" # Highest priority, requires immediate attention from the Coordinator
3497 |
3498 |
3499 | class RequestStatus(str, Enum):
3500 | """
3501 | Status options for information requests.
3502 |
3503 | Represents the lifecycle of an information request from creation to resolution.
3504 | Requests typically progress from NEW -> ACKNOWLEDGED -> IN_PROGRESS -> RESOLVED.
3505 | DEFERRED indicates requests that will be addressed later.
3506 | """
3507 |
3508 | NEW = "new" # Request has been created but not yet acknowledged by the Coordinator
3509 | ACKNOWLEDGED = "acknowledged" # Coordinator has seen the request but hasn't started working on it
3510 | IN_PROGRESS = "in_progress" # Coordinator is actively working on the request
3511 | RESOLVED = "resolved" # Coordinator has provided a resolution to the request
3512 | DEFERRED = "deferred" # Request handling has been postponed to a later time
3513 |
3514 |
3515 | class LogEntryType(str, Enum):
3516 | """
3517 | Types of log entries in the project log.
3518 |
3519 | These entry types categorize all events that can occur during a project.
3520 | Log entries provide a chronological history of actions and events in the project,
3521 | allowing both Coordinators and team members to track progress and changes.
3522 | """
3523 |
3524 | # Brief-related events
3525 | BRIEFING_CREATED = "briefing_created"
3526 | BRIEFING_UPDATED = "briefing_updated"
3527 |
3528 | # Goal-related events
3529 | GOAL_ADDED = "goal_added"
3530 | GOAL_DELETED = "goal_deleted"
3531 |
3532 | # Information request deletion event
3533 | REQUEST_DELETED = "request_deleted"
3534 |
3535 | # Information request lifecycle events
3536 | REQUEST_CREATED = "request_created"
3537 | REQUEST_UPDATED = "request_updated"
3538 | REQUEST_RESOLVED = "request_resolved"
3539 |
3540 | # Project state and progress events
3541 | STATUS_CHANGED = "status_changed"
3542 | GOAL_COMPLETED = "goal_completed"
3543 | CRITERION_COMPLETED = "criterion_completed"
3544 |
3545 | # Participant events
3546 | PARTICIPANT_JOINED = "participant_joined"
3547 | PARTICIPANT_LEFT = "participant_left"
3548 |
3549 | # Project lifecycle events
3550 | PROJECT_STARTED = "project_started"
3551 | PROJECT_COMPLETED = "project_completed"
3552 | PROJECT_ABORTED = "project_aborted"
3553 |
3554 | # Miscellaneous events
3555 | MILESTONE_PASSED = "milestone_passed"
3556 | INFORMATION_UPDATE = "information_update"
3557 | FILE_SHARED = "file_shared"
3558 | FILE_DELETED = "file_deleted"
3559 | KB_UPDATE = "kb_update"
3560 | CUSTOM = "custom"
3561 |
3562 |
3563 | class BaseEntity(BaseModel):
3564 | """
3565 | Base class for all project entities.
3566 |
3567 | Provides common fields and behavior that all project-related data models inherit.
3568 | This ensures consistency in how entities are created, versioned, and tracked.
3569 | All derived classes will have proper timestamps and creator information.
3570 | """
3571 |
3572 | version: int = 1
3573 | created_at: datetime = Field(default_factory=datetime.utcnow)
3574 | updated_at: datetime = Field(default_factory=datetime.utcnow)
3575 | created_by: str # User ID
3576 | updated_by: str # User ID
3577 | conversation_id: str # Source conversation ID
3578 |
3579 |
3580 | class SuccessCriterion(BaseModel):
3581 | """
3582 | A specific measurable criterion that defines project success.
3583 |
3584 | Success criteria are individual checkpoints that must be completed
3585 | to achieve a project goal. Each criterion represents a concrete,
3586 | verifiable action or condition that can be marked as completed.
3587 |
3588 | When all success criteria for all goals are completed, the project
3589 | can be considered successful. Team members typically report when
3590 | criteria have been met.
3591 | """
3592 |
3593 | id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique identifier for the criterion
3594 | description: str # Clear, specific description of what needs to be accomplished
3595 | completed: bool = False # Whether this criterion has been met
3596 | completed_at: Optional[datetime] = None # When the criterion was marked as completed
3597 | completed_by: Optional[str] = None # User ID of the person who completed the criterion
3598 |
3599 |
3600 | class ProjectGoal(BaseModel):
3601 | """
3602 | A specific goal for the project with associated success criteria.
3603 |
3604 | Project goals represent the major objectives that need to be accomplished
3605 | for the project to be successful. Each goal consists of a name, description,
3606 | priority level, and a list of specific success criteria that define when
3607 | the goal can be considered complete.
3608 |
3609 | Goals are typically set by the Coordinator during project planning and then tracked
3610 | by both the Coordinator and team members throughout the project.
3611 | """
3612 |
3613 | id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique identifier for the goal
3614 | name: str # Short, clear name of the goal
3615 | description: str # Detailed description of what the goal entails
3616 | priority: int = 1 # Priority level (1 = highest priority, increasing numbers = lower priority)
3617 | success_criteria: List[SuccessCriterion] = Field(default_factory=list) # List of criteria to meet
3618 |
3619 |
3620 | class ProjectBrief(BaseEntity):
3621 | """
3622 | A thorough, comprehensive documentation of the project or context to be transferred,
3623 | containing all relevant information necessary for understanding and execution.
3624 |
3625 | The brief is the primary document that defines the project or context.
3626 | It serves as the central reference for both the Coordinator and team members
3627 | to understand what needs to be accomplished and why, or in the case of context transfer,
3628 | what information needs to be preserved and communicated.
3629 |
3630 | In the standard project configuration, it includes project goals, success criteria,
3631 | and complete context. In context transfer configuration, it focuses on capturing
3632 | comprehensive context through detailed description and additional_context fields.
3633 |
3634 | Created by the Coordinator during the PLANNING phase, the brief must be
3635 | completed before the project can move to the READY_FOR_WORKING state.
3636 | Once team operations begin, the brief can still be updated,
3637 | but major changes should be communicated to all participants.
3638 | """
3639 |
3640 | title: str # Short, distinctive title for the project or context bundle to transfer
3641 | description: str # Comprehensive description of the project's or context's purpose, scope, and context
3642 | timeline: Optional[str] = None # Expected timeline or deadline information (not used in context transfer mode)
3643 | additional_context: Optional[str] = (
3644 | None # Detailed supplementary information for project participants or context transfer
3645 | )
3646 |
3647 |
3648 | class ProjectWhiteboard(BaseEntity):
3649 | """
3650 | A dynamic whiteboard that gets automatically updated as the coordinator assembles their project.
3651 |
3652 | The project whiteboard captures and maintains important project context that emerges during
3653 | conversations. It is automatically updated after each assistant message by analyzing
3654 | the conversation history and extracting key information.
3655 |
3656 | Unlike a traditional knowledge base with separate sections, the whiteboard is a single
3657 | consolidated view that shows the most relevant information for the project. It serves as
3658 | a dynamic, evolving source of truth that all team members can reference.
3659 | """
3660 |
3661 | content: str = "" # Markdown content for the whiteboard
3662 | is_auto_generated: bool = True # Whether the content was auto-generated or manually edited
3663 |
3664 |
3665 | class InformationRequest(BaseEntity):
3666 | """
3667 | A specific information need or blocker submitted by team members
3668 | that requires Coordinator support to resolve.
3669 |
3670 | Information requests are the primary communication mechanism for team members
3671 | to request assistance, information, or resources from the Coordinator. They represent
3672 | questions, blockers, or needs that arise during project execution.
3673 |
3674 | The lifecycle of an information request typically follows:
3675 | 1. Created by a team member (NEW status)
3676 | 2. Seen by the Coordinator (ACKNOWLEDGED status)
3677 | 3. Worked on by the Coordinator (IN_PROGRESS status)
3678 | 4. Completed with a resolution (RESOLVED status)
3679 |
3680 | Requests can also be DEFERRED for later handling or CANCELLED if no longer relevant.
3681 | The request priority helps the Coordinator prioritize which requests to handle first.
3682 | """
3683 |
3684 | # Request identification
3685 | request_id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique ID for this request
3686 |
3687 | # Request details
3688 | title: str # Short summary of the request
3689 | description: str # Detailed explanation of what is needed
3690 | priority: RequestPriority = RequestPriority.MEDIUM # Urgency level of the request
3691 | status: RequestStatus = RequestStatus.NEW # Current status in the request lifecycle
3692 |
3693 | # Reference to the related goal(s) if applicable
3694 | related_goal_ids: List[str] = Field(default_factory=list) # IDs of project goals this request relates to
3695 |
3696 | # Resolution information
3697 | resolution: Optional[str] = None # The answer or solution provided by the Coordinator
3698 | resolved_at: Optional[datetime] = None # When the request was resolved
3699 | resolved_by: Optional[str] = None # User ID of the Coordinator who resolved this request
3700 |
3701 | # Updates and comments on this request
3702 | updates: List[Dict[str, Any]] = Field(default_factory=list) # History of status updates and comments
3703 |
3704 |
3705 | class LogEntry(BaseModel):
3706 | """
3707 | Individual entry in the project log.
3708 |
3709 | Log entries record all significant events that occur during a project.
3710 | Each entry has a specific type, message, and associated metadata.
3711 |
3712 | The chronological sequence of log entries forms a complete audit trail
3713 | of the project's progress, actions taken, and events that occurred.
3714 | This provides accountability and helps with post-project review.
3715 |
3716 | Log entries are typically created automatically by the system when
3717 | certain actions are taken, but can also be manually added by participants.
3718 | """
3719 |
3720 | id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique identifier for this log entry
3721 | timestamp: datetime = Field(default_factory=datetime.utcnow) # When this entry was created
3722 | entry_type: LogEntryType # The category/type of this log entry
3723 | message: str # Human-readable description of what happened
3724 | user_id: str # ID of the user who performed the action or triggered the event
3725 | user_name: str # Name of the user, for display purposes
3726 |
3727 | # Optional additional context for the entry
3728 | related_entity_id: Optional[str] = None # ID of related entity (e.g., information request ID)
3729 | entity_type: Optional[str] = None # Type of related entity (e.g., "information_request", "goal")
3730 | metadata: Optional[Dict] = None # Additional structured data about the event
3731 |
3732 |
3733 | class ProjectLog(BaseModel):
3734 | """
3735 | A chronological record of all actions and interactions during the project,
3736 | including updates and progress reports.
3737 |
3738 | The project log serves as the comprehensive history of everything that
3739 | happened during a project. It contains a chronological list of log entries
3740 | describing actions, state changes, and significant events.
3741 |
3742 | The log is used for:
3743 | - Real-time monitoring of project activity
3744 | - Post-project review and analysis
3745 | - Accountability and documentation purposes
3746 | - Tracking the sequence of events leading to outcomes
3747 |
3748 | Both the Coordinator and team members can view the project log, providing transparency
3749 | into what has occurred during the project.
3750 | """
3751 |
3752 | entries: List[LogEntry] = Field(default_factory=list) # Chronological list of log entries
3753 |
3754 |
3755 | class ProjectInfo(BaseModel):
3756 | """
3757 | Core information about a project.
3758 |
3759 | This model stores essential project metadata that doesn't fit into other
3760 | specific models like brief or whiteboard. It's the central reference point
3761 | for project identification, state, and team collaboration settings.
3762 | """
3763 |
3764 | project_id: str # Unique identifier for the project
3765 | state: ProjectState = ProjectState.PLANNING # Current project lifecycle state
3766 | coordinator_conversation_id: Optional[str] = None # ID of the coordinator's conversation
3767 | team_conversation_id: Optional[str] = None # ID of the team conversation
3768 | share_url: Optional[str] = None # Shareable URL for inviting users to the team conversation
3769 | created_at: datetime = Field(default_factory=datetime.utcnow)
3770 | updated_at: datetime = Field(default_factory=datetime.utcnow)
3771 | updated_by: Optional[str] = None # User ID who last updated the project info
3772 | status_message: Optional[str] = None # Custom status message about the project
3773 | progress_percentage: Optional[int] = None # Current progress percentage (0-100)
3774 | next_actions: List[str] = Field(default_factory=list) # List of next actions planned
3775 | version: int = 1 # Version counter for tracking changes
3776 | completed_criteria: int = 0 # Count of completed success criteria
3777 | total_criteria: int = 0 # Total count of success criteria
3778 | lifecycle: Dict[str, Any] = Field(default_factory=dict) # Lifecycle metadata
3779 |
3780 |
3781 | class Project(BaseModel):
3782 | """
3783 | A comprehensive representation of a project, including its brief, whiteboard,
3784 | information requests, logs, and other related entities.
3785 |
3786 | This model encapsulates all the components that make up a project,
3787 | providing a single point of access to all relevant information.
3788 | It serves as the main interface for interacting with the project data.
3789 | """
3790 |
3791 | info: Optional[ProjectInfo]
3792 | brief: Optional[ProjectBrief]
3793 | goals: List[ProjectGoal] = Field(default_factory=list)
3794 | requests: List[InformationRequest] = Field(default_factory=list)
3795 | whiteboard: Optional[ProjectWhiteboard]
3796 | log: Optional[ProjectLog] = Field(default_factory=lambda: ProjectLog())
3797 |
3798 |
3799 | === File: assistants/project-assistant/assistant/project_files.py ===
3800 | """
3801 | Project files management module.
3802 |
3803 | This module provides functionality for sharing files between Coordinator and Team conversations.
3804 | It enables automatic synchronization of files from Coordinator to Team conversations.
3805 | """
3806 |
3807 | import asyncio
3808 | import io
3809 | import pathlib
3810 | from datetime import datetime
3811 | from typing import Dict, List, Optional
3812 |
3813 | from pydantic import BaseModel, Field
3814 | from semantic_workbench_api_model import workbench_model
3815 | from semantic_workbench_api_model.workbench_model import MessageType, NewConversationMessage
3816 | from semantic_workbench_assistant.assistant_app import ConversationContext
3817 |
3818 | from .conversation_clients import ConversationClientManager
3819 | from .conversation_project_link import ConversationProjectManager
3820 | from .logging import logger
3821 | from .project_common import detect_assistant_role
3822 | from .project_data import LogEntryType
3823 | from .project_storage import ProjectStorage, ProjectStorageManager, read_model, write_model
3824 | from .project_storage_models import ConversationRole
3825 |
3826 |
3827 | # Define helper function for safe logging without 'filename' conflict
3828 | def safe_extra(log_data):
3829 | """Create a safe extra dict for logging without LogRecord conflicts."""
3830 | # Make a copy to avoid modifying the original
3831 | safe_data = log_data.copy()
3832 |
3833 | # Rename any keys that conflict with LogRecord attributes
3834 | if "filename" in safe_data:
3835 | safe_data["file_name"] = safe_data.pop("filename")
3836 | if "module" in safe_data:
3837 | safe_data["mod_name"] = safe_data.pop("module")
3838 | if "name" in safe_data:
3839 | safe_data["obj_name"] = safe_data.pop("name")
3840 |
3841 | return safe_data
3842 |
3843 |
3844 | class ProjectFile(BaseModel):
3845 | """Metadata for a file shared within a project."""
3846 |
3847 | file_id: str
3848 | filename: str
3849 | content_type: str
3850 | file_size: int
3851 | created_by: str # User ID
3852 | created_at: datetime
3853 | updated_at: datetime
3854 | updated_by: str # User ID
3855 | is_coordinator_file: bool = True # Whether this file was created by Coordinator
3856 |
3857 |
3858 | class ProjectFileCollection(BaseModel):
3859 | """Collection of file metadata for a project."""
3860 |
3861 | files: List[ProjectFile] = Field(default_factory=list)
3862 |
3863 |
3864 | class ProjectFileManager:
3865 | """
3866 | Manages shared project files.
3867 |
3868 | Provides functionality for copying files between conversations and maintaining
3869 | a synchronized file repository for each project.
3870 | """
3871 |
3872 | @staticmethod
3873 | def get_project_files_dir(project_id: str) -> pathlib.Path:
3874 | """
3875 | Gets the directory for project files.
3876 | """
3877 | project_dir = ProjectStorageManager.get_project_dir(project_id)
3878 | files_dir = project_dir / "files"
3879 | files_dir.mkdir(parents=True, exist_ok=True)
3880 | return files_dir
3881 |
3882 | @staticmethod
3883 | def get_file_metadata_path(project_id: str) -> pathlib.Path:
3884 | """
3885 | Gets the path to the file metadata JSON.
3886 | """
3887 | files_dir = ProjectFileManager.get_project_files_dir(project_id)
3888 | return files_dir / "file_metadata.json"
3889 |
3890 | @staticmethod
3891 | def get_file_path(project_id: str, filename: str) -> pathlib.Path:
3892 | """
3893 | Gets the path to a specific file in the project.
3894 | """
3895 | files_dir = ProjectFileManager.get_project_files_dir(project_id)
3896 | return files_dir / filename
3897 |
3898 | @staticmethod
3899 | def read_file_metadata(project_id: str) -> ProjectFileCollection:
3900 | """
3901 | Reads file metadata for a project.
3902 | """
3903 | path = ProjectFileManager.get_file_metadata_path(project_id)
3904 | return read_model(path, ProjectFileCollection) or ProjectFileCollection(
3905 | files=[],
3906 | )
3907 |
3908 | @staticmethod
3909 | def write_file_metadata(project_id: str, metadata: ProjectFileCollection) -> pathlib.Path:
3910 | """
3911 | Writes file metadata for a project.
3912 | """
3913 | path = ProjectFileManager.get_file_metadata_path(project_id)
3914 | write_model(path, metadata)
3915 | return path
3916 |
3917 | @staticmethod
3918 | async def copy_file_to_project_storage(
3919 | context: ConversationContext,
3920 | project_id: str,
3921 | file: workbench_model.File,
3922 | is_coordinator_file: bool = True,
3923 | ) -> bool:
3924 | """
3925 | Copies a file from a conversation to project storage.
3926 | """
3927 | # Create safe log data for debugging
3928 | log_extra = {
3929 | "file_name": file.filename,
3930 | "project_id": project_id,
3931 | "conversation_id": str(context.id),
3932 | "file_size": getattr(file, "file_size", 0),
3933 | "is_coordinator_file": is_coordinator_file,
3934 | }
3935 |
3936 | try:
3937 | # Verify file information
3938 | if not file.filename:
3939 | logger.error("Missing filename in file metadata", extra=safe_extra(log_extra))
3940 | return False
3941 |
3942 | # Check if project storage directory exists
3943 | files_dir = ProjectFileManager.get_project_files_dir(project_id)
3944 | if not files_dir.exists():
3945 | logger.debug(f"Creating project files directory: {files_dir}", extra=safe_extra(log_extra))
3946 | files_dir.mkdir(parents=True, exist_ok=True)
3947 |
3948 | # Read the file from the conversation with error handling
3949 | try:
3950 | buffer = io.BytesIO()
3951 | async with context.read_file(file.filename) as reader:
3952 | async for chunk in reader:
3953 | buffer.write(chunk)
3954 |
3955 | # Verify we got file content
3956 | buffer_size = buffer.tell()
3957 | if buffer_size == 0:
3958 | logger.error(
3959 | "Failed to read file content from conversation - buffer is empty", extra=safe_extra(log_extra)
3960 | )
3961 | return False
3962 |
3963 | except Exception as read_error:
3964 | logger.error(f"Error reading file from conversation: {read_error}", extra=safe_extra(log_extra))
3965 | return False
3966 |
3967 | buffer.seek(0)
3968 |
3969 | # Write the file to project storage
3970 | file_path = ProjectFileManager.get_file_path(project_id, file.filename)
3971 | try:
3972 | with open(file_path, "wb") as f:
3973 | f.write(buffer.getvalue())
3974 |
3975 | # Verify file was written
3976 | if not file_path.exists() or file_path.stat().st_size == 0:
3977 | logger.error(
3978 | "Failed to write file to project storage - file is missing or empty",
3979 | extra=safe_extra(log_extra),
3980 | )
3981 | return False
3982 |
3983 | except Exception as write_error:
3984 | logger.error(f"Error writing file to project storage: {write_error}", extra=safe_extra(log_extra))
3985 | return False
3986 |
3987 | # Store file metadata
3988 | file_metadata = ProjectFile(
3989 | file_id=str(getattr(file, "id", "")),
3990 | filename=file.filename,
3991 | content_type=file.content_type,
3992 | file_size=file.file_size,
3993 | created_by=file.participant_id,
3994 | created_at=file.created_datetime,
3995 | updated_at=file.updated_datetime,
3996 | updated_by=file.participant_id,
3997 | is_coordinator_file=is_coordinator_file,
3998 | )
3999 |
4000 | # Add to metadata collection with error handling
4001 | try:
4002 | metadata_path = ProjectFileManager.get_file_metadata_path(project_id)
4003 | logger.debug(f"Reading metadata from {metadata_path}", extra=safe_extra(log_extra))
4004 |
4005 | metadata = read_model(metadata_path, ProjectFileCollection)
4006 | if not metadata:
4007 | # Create new collection
4008 | metadata = ProjectFileCollection(
4009 | files=[],
4010 | )
4011 |
4012 | # Check if file already exists in collection
4013 | existing_idx = next((i for i, f in enumerate(metadata.files) if f.filename == file.filename), None)
4014 | if existing_idx is not None:
4015 | metadata.files[existing_idx] = file_metadata
4016 | else:
4017 | metadata.files.append(file_metadata)
4018 |
4019 | # Save metadata
4020 | ProjectFileManager.write_file_metadata(project_id, metadata)
4021 |
4022 | # Verify metadata was written
4023 | if not metadata_path.exists():
4024 | logger.error(f"Failed to write metadata file {metadata_path}", extra=safe_extra(log_extra))
4025 | return False
4026 |
4027 | # Final check - verify file appears in metadata
4028 | verification_metadata = read_model(metadata_path, ProjectFileCollection)
4029 | if not verification_metadata:
4030 | logger.error("Metadata file exists but can't be read", extra=safe_extra(log_extra))
4031 | return False
4032 |
4033 | file_exists_in_metadata = any(f.filename == file.filename for f in verification_metadata.files)
4034 | if not file_exists_in_metadata:
4035 | logger.error(
4036 | f"File metadata doesn't contain entry for {file.filename}", extra=safe_extra(log_extra)
4037 | )
4038 | return False
4039 |
4040 | except Exception as metadata_error:
4041 | logger.error(f"Error updating metadata: {metadata_error}", extra=safe_extra(log_extra))
4042 | return False
4043 |
4044 | return True
4045 |
4046 | except Exception as e:
4047 | logger.exception(f"Error copying file to project storage: {e}", extra=safe_extra(log_extra))
4048 | return False
4049 |
4050 | @staticmethod
4051 | async def delete_file_from_project_storage(context: ConversationContext, project_id: str, filename: str) -> bool:
4052 | """
4053 | Deletes a file from project storage.
4054 | """
4055 | try:
4056 | # Get the file path
4057 | file_path = ProjectFileManager.get_file_path(project_id, filename)
4058 | if not file_path.exists():
4059 | return True # File doesn't exist, nothing to delete
4060 |
4061 | # Remove the file
4062 | file_path.unlink()
4063 |
4064 | # Update metadata
4065 | metadata_path = ProjectFileManager.get_file_metadata_path(project_id)
4066 | metadata = read_model(metadata_path, ProjectFileCollection)
4067 | if not metadata:
4068 | return True # No metadata to update
4069 |
4070 | # Remove the file from metadata
4071 | metadata.files = [f for f in metadata.files if f.filename != filename]
4072 |
4073 | # Save metadata
4074 | ProjectFileManager.write_file_metadata(project_id, metadata)
4075 |
4076 | # Also notify Team conversations to delete their copies
4077 | await ProjectFileManager.notify_team_conversations_file_deleted(
4078 | context=context, project_id=project_id, filename=filename
4079 | )
4080 |
4081 | return True
4082 |
4083 | except Exception as e:
4084 | logger.exception(f"Error deleting file from project storage: {e}")
4085 | return False
4086 |
4087 | @staticmethod
4088 | async def notify_team_conversations_file_deleted(
4089 | context: ConversationContext, project_id: str, filename: str
4090 | ) -> None:
4091 | """
4092 | Notifies Team conversations to delete a file that was deleted by the Coordinator.
4093 | """
4094 | try:
4095 | # Get Team conversations
4096 | team_conversations = await ProjectFileManager.get_team_conversations(context, project_id)
4097 | if not team_conversations:
4098 | return
4099 |
4100 | for conv_id in team_conversations:
4101 | try:
4102 | client = ConversationClientManager.get_conversation_client(context, conv_id)
4103 |
4104 | # Check if file exists in the conversation
4105 | conversation = await client.get_conversation()
4106 | files = getattr(conversation, "files", [])
4107 | file_exists = any(f.filename == filename for f in files)
4108 |
4109 | if file_exists:
4110 | # Delete the file
4111 | await client.delete_file(filename)
4112 | logger.debug(f"Deleted file {filename} from Team conversation {conv_id}")
4113 |
4114 | # Send notification
4115 | await client.send_messages(
4116 | NewConversationMessage(
4117 | content=f"Coordinator deleted a shared file: {filename}",
4118 | message_type=MessageType.notice,
4119 | )
4120 | )
4121 | except Exception as e:
4122 | logger.warning(f"Failed to delete file {filename} from Team conversation {conv_id}: {e}")
4123 |
4124 | except Exception as e:
4125 | logger.exception(f"Error notifying Team conversations about deleted file: {e}")
4126 |
4127 | @staticmethod
4128 | async def copy_file_to_conversation(
4129 | context: ConversationContext,
4130 | project_id: str,
4131 | filename: str,
4132 | target_conversation_id: str,
4133 | ) -> bool:
4134 | """
4135 | Copies a file from project storage to a target conversation.
4136 | """
4137 | try:
4138 | # Check if the file exists in project storage
4139 | file_path = ProjectFileManager.get_file_path(project_id, filename)
4140 | if not file_path.exists():
4141 | logger.warning(f"File {filename} not found in project storage")
4142 | return False
4143 |
4144 | # Get file metadata
4145 | metadata = ProjectFileManager.read_file_metadata(project_id)
4146 | if not metadata:
4147 | logger.warning(f"No file metadata found for project {project_id}")
4148 | return False
4149 |
4150 | # Find the file metadata
4151 | file_meta = next((f for f in metadata.files if f.filename == filename), None)
4152 | if not file_meta:
4153 | logger.warning(f"No metadata found for file {filename}")
4154 | return False
4155 |
4156 | # Create client for target conversation
4157 | target_client = ConversationClientManager.get_conversation_client(context, target_conversation_id)
4158 | if not target_client:
4159 | logger.warning(f"Could not create client for conversation {target_conversation_id}")
4160 | return False
4161 |
4162 | # Read the file content
4163 | try:
4164 | with open(file_path, "rb") as f:
4165 | file_bytes = f.read()
4166 |
4167 | if not file_bytes:
4168 | logger.warning(f"Failed to read file content from {file_path} (empty file)")
4169 | return False
4170 |
4171 | file_content = io.BytesIO(file_bytes)
4172 | except Exception as read_error:
4173 | logger.error(f"Failed to read file: {read_error}")
4174 | return False
4175 |
4176 | # Determine content type
4177 | content_type = file_meta.content_type
4178 | if not content_type:
4179 | content_type = "application/octet-stream"
4180 |
4181 | # Check if the file exists and delete it first (to handle updates)
4182 | try:
4183 | conversation = await target_client.get_conversation()
4184 | target_files = getattr(conversation, "files", [])
4185 | file_exists = any(f.filename == filename for f in target_files)
4186 |
4187 | if file_exists:
4188 | logger.debug(f"File {filename} exists, deleting before upload")
4189 | await target_client.delete_file(filename)
4190 |
4191 | # Brief wait after deletion
4192 | await asyncio.sleep(1.0)
4193 | except Exception as e:
4194 | logger.error(f"Could not check/delete existing file: {e}")
4195 | # Continue with upload anyway
4196 |
4197 | # Upload the file
4198 | try:
4199 | file_content.seek(0) # Reset position to start of file
4200 | await target_client.write_file(filename=filename, file_content=file_content, content_type=content_type)
4201 | logger.debug(f"Successfully uploaded file {filename}")
4202 | return True
4203 | except Exception as upload_error:
4204 | logger.error(f"Failed to upload file: {upload_error}")
4205 | return False
4206 |
4207 | except Exception as e:
4208 | logger.exception(f"Error copying file to conversation: {e}")
4209 | return False
4210 |
4211 | @staticmethod
4212 | async def get_team_conversations(context: ConversationContext, project_id: str) -> List[str]:
4213 | """
4214 | Gets all Team conversation IDs for a project.
4215 | """
4216 | try:
4217 | # Get linked conversations
4218 | linked_conversations = await ConversationProjectManager.get_linked_conversations(context)
4219 |
4220 | # Filter for team conversations
4221 | team_conversations = []
4222 | for conv_id in linked_conversations:
4223 | # Check if this is a team conversation
4224 | temp_context = await ProjectFileManager.create_temporary_context(context, conv_id)
4225 | if temp_context:
4226 | role = await ConversationProjectManager.get_conversation_role(temp_context)
4227 | if role == ConversationRole.TEAM:
4228 | team_conversations.append(conv_id)
4229 |
4230 | return team_conversations
4231 |
4232 | except Exception as e:
4233 | logger.exception(f"Error getting team conversations: {e}")
4234 | return []
4235 |
4236 | @staticmethod
4237 | async def create_temporary_context(
4238 | source_context: ConversationContext, target_conversation_id: str
4239 | ) -> Optional[ConversationContext]:
4240 | """
4241 | Creates a temporary context for a target conversation.
4242 | """
4243 | try:
4244 | return await ConversationClientManager.create_temporary_context_for_conversation(
4245 | source_context, target_conversation_id
4246 | )
4247 |
4248 | except Exception as e:
4249 | logger.exception(f"Error creating temporary context: {e}")
4250 | return None
4251 |
4252 | @staticmethod
4253 | async def synchronize_files_to_team_conversation(
4254 | context: ConversationContext,
4255 | project_id: str,
4256 | ) -> None:
4257 | """
4258 | Synchronize all project files to a Team conversation.
4259 | """
4260 | logger.debug(f"Starting file synchronization for project {project_id}")
4261 |
4262 | # Get file metadata for the project
4263 | metadata = ProjectFileManager.read_file_metadata(project_id)
4264 |
4265 | if not metadata or not metadata.files:
4266 | # No metadata found
4267 | await context.send_messages(
4268 | NewConversationMessage(
4269 | content="No shared files available. The coordinator hasn't shared any files yet.",
4270 | message_type=MessageType.notice,
4271 | )
4272 | )
4273 |
4274 | # Identify Coordinator files to sync
4275 | coordinator_files = [f for f in metadata.files if f.is_coordinator_file]
4276 |
4277 | # Check which files already exist in conversation
4278 | conversation = await context.get_conversation()
4279 | existing_files = getattr(conversation, "files", [])
4280 | existing_filenames = {f.filename for f in existing_files}
4281 |
4282 | # Track successful and failed files
4283 | successful_files = []
4284 | failed_files = []
4285 | skipped_files = [] # Files that already exist
4286 |
4287 | # Process each file
4288 | for file_meta in coordinator_files:
4289 | # Skip files that already exist
4290 | if file_meta.filename in existing_filenames:
4291 | skipped_files.append(file_meta.filename)
4292 | continue
4293 |
4294 | # Try to copy the file
4295 | success = await ProjectFileManager.copy_file_to_conversation(
4296 | context=context,
4297 | project_id=project_id,
4298 | filename=file_meta.filename,
4299 | target_conversation_id=str(context.id),
4300 | )
4301 |
4302 | if success:
4303 | successful_files.append(file_meta.filename)
4304 | else:
4305 | failed_files.append(file_meta.filename)
4306 |
4307 | # Create notification message for the user
4308 | available_files = successful_files + skipped_files
4309 | if available_files:
4310 | # Create message about synchronized files
4311 | if successful_files:
4312 | file_list = ", ".join(successful_files)
4313 | message = f"Synchronized files from Coordinator: {file_list}"
4314 |
4315 | # Add info about skipped files if any
4316 | if skipped_files:
4317 | existing_list = ", ".join(skipped_files)
4318 | message += f"\nAlready available: {existing_list}"
4319 | else:
4320 | # Only skipped files
4321 | file_list = ", ".join(skipped_files)
4322 | message = f"All shared files already available: {file_list}"
4323 |
4324 | # Send notification
4325 | await context.send_messages(
4326 | NewConversationMessage(
4327 | content=message,
4328 | message_type=MessageType.notice,
4329 | )
4330 | )
4331 |
4332 | # Log the synchronization event
4333 | sync_message = (
4334 | f"Synchronized files to Team conversation: {len(successful_files)} new, {len(skipped_files)} existing"
4335 | )
4336 |
4337 | await ProjectStorage.log_project_event(
4338 | context=context,
4339 | project_id=project_id,
4340 | entry_type=LogEntryType.FILE_SHARED,
4341 | message=sync_message,
4342 | metadata={
4343 | "successful_files": successful_files,
4344 | "skipped_files": skipped_files,
4345 | "failed_files": failed_files,
4346 | },
4347 | )
4348 |
4349 | @staticmethod
4350 | async def get_shared_files(context: ConversationContext, project_id: str) -> Dict[str, ProjectFile]:
4351 | """
4352 | Gets all shared files for a project with filename as key.
4353 | """
4354 | try:
4355 | # Get file metadata for the project
4356 | metadata = ProjectFileManager.read_file_metadata(project_id)
4357 | if not metadata or not metadata.files:
4358 | return {}
4359 |
4360 | # Create dictionary with filename as key
4361 | files_dict = {f.filename: f for f in metadata.files}
4362 | return files_dict
4363 |
4364 | except Exception as e:
4365 | logger.exception(f"Error getting shared files: {e}")
4366 | return {}
4367 |
4368 | @staticmethod
4369 | async def process_file_update_notification(
4370 | context: ConversationContext, project_id: str, update_type: str, filename: str
4371 | ) -> bool:
4372 | """
4373 | Processes a file update notification in a Team conversation.
4374 | """
4375 | try:
4376 | # First verify that this is a Team conversation
4377 | role = await detect_assistant_role(context)
4378 |
4379 | if role != ConversationRole.TEAM:
4380 | logger.warning("Only Team conversations should process file update notifications")
4381 | return False
4382 |
4383 | # Process based on update type
4384 | if update_type == "file_created" or update_type == "file_updated":
4385 | # Synchronize the specific file from project storage
4386 | success = await ProjectFileManager.copy_file_to_conversation(
4387 | context=context, project_id=project_id, filename=filename, target_conversation_id=str(context.id)
4388 | )
4389 |
4390 | action = "added" if update_type == "file_created" else "updated"
4391 | if success:
4392 | return True
4393 | else:
4394 | logger.warning(f"Failed to {action} file {filename} in Team conversation {context.id}")
4395 | return False
4396 |
4397 | elif update_type == "file_deleted":
4398 | # Delete the file from this conversation
4399 | try:
4400 | # Check if file exists
4401 | conversation = await context.get_conversation()
4402 | files = getattr(conversation, "files", [])
4403 | file_exists = files and any(f.filename == filename for f in files)
4404 |
4405 | if file_exists:
4406 | # Delete the file
4407 | await context.delete_file(filename)
4408 | logger.debug(f"Deleted file {filename} from Team conversation {context.id}")
4409 | return True
4410 | else:
4411 | # File doesn't exist, nothing to do
4412 | return True
4413 |
4414 | except Exception as e:
4415 | logger.warning(f"Failed to delete file {filename} from Team conversation: {e}")
4416 | return False
4417 | else:
4418 | logger.warning(f"Unknown file update type: {update_type}")
4419 | return False
4420 |
4421 | except Exception as e:
4422 | logger.exception(f"Error processing file update notification: {e}")
4423 | return False
4424 |
4425 |
4426 | === File: assistants/project-assistant/assistant/project_manager.py ===
4427 | """
4428 | Project management logic for working with project data.
4429 |
4430 | This module provides the core business logic for working with project data
4431 | """
4432 |
4433 | import re
4434 | import uuid
4435 | from datetime import datetime
4436 | from typing import List, Optional, Tuple
4437 |
4438 | import openai_client
4439 | from semantic_workbench_api_model.workbench_model import (
4440 | ConversationPermission,
4441 | MessageType,
4442 | NewConversation,
4443 | NewConversationMessage,
4444 | NewConversationShare,
4445 | ParticipantRole,
4446 | )
4447 | from semantic_workbench_assistant.assistant_app import ConversationContext
4448 |
4449 | from .config import assistant_config
4450 | from .conversation_clients import ConversationClientManager
4451 | from .conversation_project_link import ConversationProjectManager
4452 | from .logging import logger
4453 | from .project_data import (
4454 | InformationRequest,
4455 | LogEntryType,
4456 | Project,
4457 | ProjectBrief,
4458 | ProjectGoal,
4459 | ProjectInfo,
4460 | ProjectLog,
4461 | ProjectState,
4462 | ProjectWhiteboard,
4463 | RequestPriority,
4464 | RequestStatus,
4465 | SuccessCriterion,
4466 | )
4467 | from .project_notifications import ProjectNotifier
4468 | from .project_storage import ProjectStorage, ProjectStorageManager
4469 | from .project_storage_models import ConversationRole
4470 | from .utils import get_current_user, require_current_user
4471 |
4472 |
4473 | class ProjectManager:
4474 | """
4475 | Manages the creation, modification, and lifecycle of projects.
4476 |
4477 | The ProjectManager provides a centralized set of operations for working with project data.
4478 | It handles all the core business logic for interacting with projects, ensuring that
4479 | operations are performed consistently and following the proper rules and constraints.
4480 |
4481 | This class implements the primary interface for both Coordinators and team members to interact
4482 | with project entities like briefs, information requests, and knowledge bases. It abstracts
4483 | away the storage details and provides a clean API for project operations.
4484 |
4485 | All methods are implemented as static methods to facilitate easy calling from
4486 | different parts of the codebase without requiring instance creation.
4487 | """
4488 |
4489 | @staticmethod
4490 | async def create_shareable_team_conversation(context: ConversationContext, project_id: str) -> str:
4491 | """
4492 | Creates a new shareable team conversation template.
4493 |
4494 | This creates a new conversation owned by the same user as the current conversation,
4495 | intended to be used as a shareable team conversation template. This is NOT a
4496 | conversation that anyone will directly use. Instead, it's a template that gets
4497 | copied when team members redeem the share URL, creating their own individual
4498 | team conversations.
4499 |
4500 | The conversation is tagged with metadata indicating its purpose and gets a
4501 | share URL that can be used by team members to join the project.
4502 |
4503 | Args:
4504 | context: Current conversation context
4505 | project_id: ID of the project
4506 |
4507 | Returns:
4508 | share_url: URL for joining a team conversation
4509 | """
4510 |
4511 | # Get the current user ID to set as owner
4512 | user_id, _ = await get_current_user(context)
4513 | if not user_id:
4514 | raise ValueError("Cannot create team conversation: no user found")
4515 |
4516 | new_conversation = NewConversation(
4517 | metadata={
4518 | "is_team_conversation": True,
4519 | "project_id": project_id,
4520 | "setup_complete": True,
4521 | "project_role": "team",
4522 | "assistant_mode": "team",
4523 | },
4524 | )
4525 | client = context._conversations_client
4526 | conversation = await client.create_conversation_with_owner(new_conversation=new_conversation, owner_id=user_id)
4527 |
4528 | if not conversation or not conversation.id:
4529 | raise ValueError("Failed to create team conversation")
4530 |
4531 | new_share = NewConversationShare(
4532 | conversation_id=conversation.id,
4533 | label="Join Team Conversation",
4534 | conversation_permission=ConversationPermission.read,
4535 | metadata={
4536 | "project_id": project_id,
4537 | "is_team_conversation": True,
4538 | "showDuplicateAction": True,
4539 | "show_duplicate_action": True,
4540 | },
4541 | )
4542 | share = await context._conversations_client.create_conversation_share_with_owner(
4543 | new_conversation_share=new_share, owner_id=user_id
4544 | )
4545 |
4546 | share_url = f"/conversation-share/{share.id}/redeem"
4547 |
4548 | # Store team conversation info in ProjectInfo
4549 | project_info = ProjectStorage.read_project_info(project_id)
4550 | if project_info:
4551 | project_info.team_conversation_id = str(conversation.id)
4552 | project_info.share_url = share_url
4553 | project_info.updated_at = datetime.utcnow()
4554 | ProjectStorage.write_project_info(project_id, project_info)
4555 | else:
4556 | raise ValueError(f"Project info not found for project ID: {project_id}")
4557 |
4558 | return share_url
4559 |
4560 | @staticmethod
4561 | async def create_project(context: ConversationContext) -> str:
4562 | """
4563 | Creates a new project and associates the current conversation with it.
4564 |
4565 | This is the initial step in project creation. It:
4566 | 1. Generates a unique project ID
4567 | 2. Associates the current conversation with that project
4568 | 3. Sets the current conversation as Coordinator for the project
4569 | 4. Creates empty project data structures (brief, whiteboard, etc.)
4570 | 5. Logs the project creation event
4571 |
4572 | After creating a project, the Coordinator should proceed to create a project brief
4573 | with specific goals and success criteria.
4574 |
4575 | Args:
4576 | context: Current conversation context containing user/assistant information
4577 |
4578 | Returns:
4579 | Tuple of (success, project_id) where:
4580 | - success: Boolean indicating if the creation was successful
4581 | - project_id: If successful, the UUID of the newly created project
4582 | """
4583 |
4584 | # Generate a unique project ID
4585 | project_id = str(uuid.uuid4())
4586 |
4587 | # Create the project directory structure first
4588 | project_dir = ProjectStorageManager.get_project_dir(project_id)
4589 | logger.debug(f"Created project directory: {project_dir}")
4590 |
4591 | # Create and save the initial project info
4592 | project_info = ProjectInfo(project_id=project_id, coordinator_conversation_id=str(context.id))
4593 |
4594 | # Save the project info
4595 | ProjectStorage.write_project_info(project_id, project_info)
4596 | logger.debug(f"Created and saved project info: {project_info}")
4597 |
4598 | # Associate the conversation with the project
4599 | logger.debug(f"Associating conversation {context.id} with project {project_id}")
4600 | await ConversationProjectManager.associate_conversation_with_project(context, project_id)
4601 |
4602 | # No need to set conversation role in project storage, as we use metadata
4603 | logger.debug(f"Conversation {context.id} is Coordinator for project {project_id}")
4604 |
4605 | # Ensure linked_conversations directory exists
4606 | linked_dir = ProjectStorageManager.get_linked_conversations_dir(project_id)
4607 | logger.debug(f"Ensured linked_conversations directory exists: {linked_dir}")
4608 |
4609 | return project_id
4610 |
4611 | @staticmethod
4612 | async def join_project(
4613 | context: ConversationContext,
4614 | project_id: str,
4615 | role: ConversationRole = ConversationRole.TEAM,
4616 | ) -> bool:
4617 | """
4618 | Joins an existing project.
4619 |
4620 | Args:
4621 | context: Current conversation context
4622 | project_id: ID of the project to join
4623 | role: Role for this conversation (COORDINATOR or TEAM)
4624 |
4625 | Returns:
4626 | True if joined successfully, False otherwise
4627 | """
4628 | try:
4629 | # Check if project exists
4630 | if not ProjectStorageManager.project_exists(project_id):
4631 | logger.error(f"Cannot join project: project {project_id} does not exist")
4632 | return False
4633 |
4634 | # Associate the conversation with the project
4635 | await ConversationProjectManager.associate_conversation_with_project(context, project_id)
4636 |
4637 | # Role is set in metadata, not in storage
4638 |
4639 | logger.info(f"Joined project {project_id} as {role.value}")
4640 | return True
4641 |
4642 | except Exception as e:
4643 | logger.exception(f"Error joining project: {e}")
4644 | return False
4645 |
4646 | @staticmethod
4647 | async def get_project_id(context: ConversationContext) -> Optional[str]:
4648 | """
4649 | Gets the project ID associated with the current conversation.
4650 |
4651 | Every conversation that's part of a project has an associated project ID.
4652 | This method retrieves that ID, which is used for accessing project-related
4653 | data structures.
4654 |
4655 | Args:
4656 | context: Current conversation context
4657 |
4658 | Returns:
4659 | The project ID string if the conversation is part of a project, None otherwise
4660 | """
4661 | return await ConversationProjectManager.get_associated_project_id(context)
4662 |
4663 | @staticmethod
4664 | async def get_project_role(context: ConversationContext) -> Optional[ConversationRole]:
4665 | """
4666 | Gets the role of the current conversation in its project.
4667 |
4668 | Each conversation participating in a project has a specific role:
4669 | - COORDINATOR: The primary conversation that created and manages the project
4670 | - TEAM: Conversations where team members are carrying out the project tasks
4671 |
4672 | This method examines the conversation metadata to determine the role
4673 | of the current conversation in the project. The role is stored in the
4674 | conversation metadata as "project_role".
4675 |
4676 | Args:
4677 | context: Current conversation context
4678 |
4679 | Returns:
4680 | The role (ProjectRole.COORDINATOR or ProjectRole.TEAM) if the conversation
4681 | is part of a project, None otherwise
4682 | """
4683 | try:
4684 | conversation = await context.get_conversation()
4685 | metadata = conversation.metadata or {}
4686 | role_str = metadata.get("project_role", "coordinator")
4687 |
4688 | if role_str == "team":
4689 | return ConversationRole.TEAM
4690 | elif role_str == "coordinator":
4691 | return ConversationRole.COORDINATOR
4692 | else:
4693 | return None
4694 | except Exception as e:
4695 | logger.exception(f"Error detecting project role: {e}")
4696 | # Default to None if we can't determine
4697 | return None
4698 |
4699 | @staticmethod
4700 | async def get_project_brief(context: ConversationContext) -> Optional[ProjectBrief]:
4701 | """
4702 | Gets the project brief for the current conversation's project.
4703 |
4704 | The project brief contains the core information about the project:
4705 | name, description, goals, and success criteria. This is the central
4706 | document that defines what the project is trying to accomplish.
4707 |
4708 | Args:
4709 | context: Current conversation context
4710 |
4711 | Returns:
4712 | The ProjectBrief object if found, None if the conversation is not
4713 | part of a project or if no brief has been created yet
4714 | """
4715 | project_id = await ProjectManager.get_project_id(context)
4716 | if not project_id:
4717 | return None
4718 |
4719 | return ProjectStorage.read_project_brief(project_id)
4720 |
4721 | @staticmethod
4722 | async def update_project_brief(
4723 | context: ConversationContext,
4724 | title: str,
4725 | description: str,
4726 | timeline: Optional[str] = None,
4727 | additional_context: Optional[str] = None,
4728 | send_notification: bool = True,
4729 | ) -> Optional[ProjectBrief]:
4730 | """
4731 | Creates or updates a project brief for the current project.
4732 |
4733 | The project brief is the primary document that defines the project for team members.
4734 |
4735 | Goals should be managed separately through add_project_goal and are not handled by this method.
4736 |
4737 | Args:
4738 | context: A reference to the conversation context object
4739 | title: Short, descriptive name for the project
4740 | description: Comprehensive description of the project's purpose
4741 | timeline: Optional information about project timeline/deadlines
4742 | additional_context: Optional additional information relevant to the project
4743 | send_notification: Whether to send a notification about the brief update (default: True)
4744 |
4745 | Returns:
4746 | The updated ProjectBrief object if successful, None otherwise
4747 | """
4748 | # Get project ID
4749 | project_id = await ProjectManager.get_project_id(context)
4750 | if not project_id:
4751 | logger.error("Cannot update brief: no project associated with this conversation")
4752 | return
4753 | # Get user information
4754 | current_user_id = await require_current_user(context, "update brief")
4755 | if not current_user_id:
4756 | return
4757 |
4758 | # Create the project brief
4759 | brief = ProjectBrief(
4760 | title=title,
4761 | description=description,
4762 | timeline=timeline,
4763 | additional_context=additional_context,
4764 | created_by=current_user_id,
4765 | updated_by=current_user_id,
4766 | conversation_id=str(context.id),
4767 | )
4768 |
4769 | # Save the brief
4770 | ProjectStorage.write_project_brief(project_id, brief)
4771 |
4772 | # Check if this is a creation or an update
4773 | existing_brief = ProjectStorage.read_project_brief(project_id)
4774 | if existing_brief:
4775 | # This is an update
4776 | await ProjectStorage.log_project_event(
4777 | context=context,
4778 | project_id=project_id,
4779 | entry_type=LogEntryType.BRIEFING_UPDATED.value,
4780 | message=f"Updated brief: {title}",
4781 | )
4782 | else:
4783 | # This is a creation
4784 | await ProjectStorage.log_project_event(
4785 | context=context,
4786 | project_id=project_id,
4787 | entry_type=LogEntryType.BRIEFING_CREATED.value,
4788 | message=f"Created brief: {title}",
4789 | )
4790 |
4791 | # Only notify if send_notification is True
4792 | if send_notification:
4793 | # Notify linked conversations
4794 | await ProjectNotifier.notify_project_update(
4795 | context=context,
4796 | project_id=project_id,
4797 | update_type="brief",
4798 | message=f"Brief created: {title}",
4799 | )
4800 |
4801 | return brief
4802 |
4803 | @staticmethod
4804 | async def get_project_state(
4805 | context: ConversationContext,
4806 | ) -> Optional[ProjectState]:
4807 | """Gets the project state for the current conversation's project."""
4808 | project_id = await ProjectManager.get_project_id(context)
4809 | if not project_id:
4810 | return None
4811 |
4812 | # Get the project info which contains state information
4813 | project_info = ProjectStorage.read_project_info(project_id)
4814 | if not project_info:
4815 | return None
4816 |
4817 | return project_info.state
4818 |
4819 | @staticmethod
4820 | async def add_project_goal(
4821 | context: ConversationContext,
4822 | goal_name: str,
4823 | goal_description: str,
4824 | success_criteria: Optional[List[str]] = None,
4825 | priority: int = 1,
4826 | ) -> Optional[ProjectGoal]:
4827 | """
4828 | Adds a goal to the project.
4829 |
4830 | Args:
4831 | context: Current conversation context
4832 | goal_name: Name of the goal
4833 | goal_description: Description of the goal
4834 | success_criteria: List of success criteria strings (optional)
4835 | priority: Priority of the goal (default: 1)
4836 |
4837 | Returns:
4838 | The created ProjectGoal if successful, None otherwise
4839 | """
4840 | # Get project ID
4841 | project_id = await ProjectManager.get_project_id(context)
4842 | if not project_id:
4843 | logger.error("Cannot add goal: no project associated with this conversation")
4844 | return None
4845 |
4846 | # Get user information
4847 | current_user_id = await require_current_user(context, "add goal")
4848 | if not current_user_id:
4849 | return None
4850 |
4851 | # Create success criteria objects if provided
4852 | criterion_objects = []
4853 | if success_criteria:
4854 | for criterion in success_criteria:
4855 | criterion_objects.append(SuccessCriterion(description=criterion))
4856 |
4857 | # Create the new goal
4858 | new_goal = ProjectGoal(
4859 | name=goal_name,
4860 | description=goal_description,
4861 | priority=priority,
4862 | success_criteria=criterion_objects,
4863 | )
4864 |
4865 | # Get the existing project
4866 | project = ProjectStorage.read_project(project_id)
4867 | if not project:
4868 | # Create a new project if it doesn't exist
4869 | project = Project(
4870 | info=None,
4871 | brief=None,
4872 | goals=[new_goal],
4873 | whiteboard=None,
4874 | requests=[],
4875 | )
4876 | else:
4877 | # Add the goal to the existing project
4878 | project.goals.append(new_goal)
4879 |
4880 | # Save the updated project
4881 | ProjectStorage.write_project(project_id, project)
4882 |
4883 | # Log the goal addition
4884 | await ProjectStorage.log_project_event(
4885 | context=context,
4886 | project_id=project_id,
4887 | entry_type=LogEntryType.GOAL_ADDED.value,
4888 | message=f"Added goal: {goal_name}",
4889 | )
4890 |
4891 | # Notify linked conversations
4892 | await ProjectNotifier.notify_project_update(
4893 | context=context,
4894 | project_id=project_id,
4895 | update_type="goal",
4896 | message=f"Goal added: {goal_name}",
4897 | )
4898 |
4899 | return new_goal
4900 |
4901 | @staticmethod
4902 | async def delete_project_goal(
4903 | context: ConversationContext,
4904 | goal_index: int,
4905 | ) -> Tuple[bool, Optional[str]]:
4906 | """
4907 | Deletes a goal from the project.
4908 |
4909 | Args:
4910 | context: Current conversation context
4911 | goal_index: The index of the goal to delete (0-based)
4912 |
4913 | Returns:
4914 | Tuple of (success, goal_name_or_error_message)
4915 | """
4916 | # Get project ID
4917 | project_id = await ProjectManager.get_project_id(context)
4918 | if not project_id:
4919 | logger.error("Cannot delete goal: no project associated with this conversation")
4920 | return False, "No project associated with this conversation."
4921 |
4922 | # Get user information
4923 | current_user_id = await require_current_user(context, "delete goal")
4924 | if not current_user_id:
4925 | return False, "Could not identify current user."
4926 |
4927 | # Get the existing project
4928 | project = ProjectStorage.read_project(project_id)
4929 | if not project or not project.goals:
4930 | return False, "No project goals found."
4931 |
4932 | # Validate index
4933 | if goal_index < 0 or goal_index >= len(project.goals):
4934 | return (
4935 | False,
4936 | f"Invalid goal index {goal_index}. Valid indexes are 0 to {len(project.goals) - 1}. There are {len(project.goals)} goals.",
4937 | )
4938 |
4939 | # Get the goal to delete
4940 | goal = project.goals[goal_index]
4941 | goal_name = goal.name
4942 |
4943 | # Remove the goal from the list
4944 | project.goals.pop(goal_index)
4945 |
4946 | # Save the updated project
4947 | ProjectStorage.write_project(project_id, project)
4948 |
4949 | # Log the goal deletion
4950 | await ProjectStorage.log_project_event(
4951 | context=context,
4952 | project_id=project_id,
4953 | entry_type=LogEntryType.GOAL_DELETED.value,
4954 | message=f"Deleted goal: {goal_name}",
4955 | )
4956 |
4957 | # Notify linked conversations
4958 | await ProjectNotifier.notify_project_update(
4959 | context=context,
4960 | project_id=project_id,
4961 | update_type="goal",
4962 | message=f"Goal deleted: {goal_name}",
4963 | )
4964 |
4965 | # Update project info with new criteria counts
4966 | project_info = ProjectStorage.read_project_info(project_id)
4967 | if project_info:
4968 | # Count all completed criteria
4969 | completed_criteria = 0
4970 | total_criteria = 0
4971 |
4972 | # Get the updated project to access goals
4973 | updated_project = ProjectStorage.read_project(project_id)
4974 | if updated_project and updated_project.goals:
4975 | for g in updated_project.goals:
4976 | total_criteria += len(g.success_criteria)
4977 | completed_criteria += sum(1 for c in g.success_criteria if c.completed)
4978 |
4979 | # Update project info with criteria stats
4980 | project_info.completed_criteria = completed_criteria
4981 | project_info.total_criteria = total_criteria
4982 |
4983 | # Calculate progress percentage
4984 | if total_criteria > 0:
4985 | project_info.progress_percentage = int((completed_criteria / total_criteria) * 100)
4986 | else:
4987 | project_info.progress_percentage = 0
4988 |
4989 | # Update metadata
4990 | project_info.updated_at = datetime.utcnow()
4991 | project_info.updated_by = current_user_id
4992 | project_info.version += 1
4993 |
4994 | # Save the updated project info
4995 | ProjectStorage.write_project_info(project_id, project_info)
4996 |
4997 | # Update all project UI inspectors
4998 | await ProjectStorage.refresh_all_project_uis(context, project_id)
4999 |
5000 | return True, goal_name
5001 |
5002 | @staticmethod
5003 | async def get_project_criteria(context: ConversationContext) -> List[SuccessCriterion]:
5004 | """
5005 | Gets the success criteria for the current conversation's project.
5006 |
5007 | Args:
5008 | context: Current conversation context
5009 | completed_only: If True, only return completed criteria
5010 |
5011 | Returns:
5012 | List of SuccessCriterion objects
5013 | """
5014 | project_id = await ProjectManager.get_project_id(context)
5015 | if not project_id:
5016 | return []
5017 |
5018 | # Get the project which contains goals and success criteria
5019 | project = ProjectStorage.read_project(project_id)
5020 | if not project:
5021 | return []
5022 |
5023 | goals = project.goals
5024 | criteria = []
5025 | for goal in goals:
5026 | # Add success criteria from each goal
5027 | criteria.extend(goal.success_criteria)
5028 |
5029 | return criteria
5030 |
5031 | @staticmethod
5032 | async def update_project_info(
5033 | context: ConversationContext,
5034 | state: Optional[str] = None,
5035 | progress: Optional[int] = None,
5036 | status_message: Optional[str] = None,
5037 | next_actions: Optional[List[str]] = None,
5038 | ) -> Optional[ProjectInfo]:
5039 | """
5040 | Updates the project info with state, progress, status message, and next actions.
5041 |
5042 | Args:
5043 | context: Current conversation context
5044 | state: Optional project state
5045 | progress: Optional progress percentage (0-100)
5046 | status_message: Optional status message
5047 | next_actions: Optional list of next actions
5048 |
5049 | Returns:
5050 | Tuple of (success, project_info)
5051 | """
5052 | # Get project ID
5053 | project_id = await ProjectManager.get_project_id(context)
5054 | if not project_id:
5055 | logger.error("Cannot update project info: no project associated with this conversation")
5056 | return None
5057 |
5058 | # Get user information
5059 | current_user_id = await require_current_user(context, "update project info")
5060 | if not current_user_id:
5061 | return None
5062 |
5063 | # Get existing project info
5064 | project_info = ProjectStorage.read_project_info(project_id)
5065 | if not project_info:
5066 | logger.error(f"Cannot update project info: no project info found for {project_id}")
5067 | return None
5068 |
5069 | # Apply updates
5070 | if state:
5071 | project_info.state = ProjectState(state)
5072 |
5073 | if status_message:
5074 | project_info.status_message = status_message
5075 |
5076 | if progress is not None:
5077 | project_info.progress_percentage = progress
5078 |
5079 | if next_actions:
5080 | if not hasattr(project_info, "next_actions"):
5081 | project_info.next_actions = []
5082 | project_info.next_actions = next_actions
5083 |
5084 | # Update metadata
5085 | project_info.updated_at = datetime.utcnow()
5086 | project_info.updated_by = current_user_id
5087 |
5088 | # Increment version if it exists
5089 | if hasattr(project_info, "version"):
5090 | project_info.version += 1
5091 |
5092 | # Save the project info
5093 | ProjectStorage.write_project_info(project_id, project_info)
5094 |
5095 | # Log the update
5096 | event_type = LogEntryType.STATUS_CHANGED
5097 | message = f"Updated project status to {project_info.state.value}"
5098 | if progress is not None:
5099 | message += f" ({progress}% complete)"
5100 |
5101 | await ProjectStorage.log_project_event(
5102 | context=context,
5103 | project_id=project_id,
5104 | entry_type=event_type.value,
5105 | message=message,
5106 | metadata={
5107 | "state": project_info.state.value,
5108 | "status_message": status_message,
5109 | "progress": progress,
5110 | },
5111 | )
5112 |
5113 | # Notify linked conversations
5114 | await ProjectNotifier.notify_project_update(
5115 | context=context,
5116 | project_id=project_id,
5117 | update_type="project_info",
5118 | message=f"Project status updated: {project_info.state.value}",
5119 | )
5120 |
5121 | return project_info
5122 |
5123 | @staticmethod
5124 | async def update_project_state(
5125 | context: ConversationContext,
5126 | state: Optional[str] = None,
5127 | status_message: Optional[str] = None,
5128 | ) -> Tuple[bool, Optional[ProjectInfo]]:
5129 | """
5130 | Updates the project state and status message.
5131 |
5132 | Args:
5133 | context: Current conversation context
5134 | state: Optional project state
5135 | status_message: Optional status message
5136 |
5137 | Returns:
5138 | Tuple of (success, project_info)
5139 | """
5140 | try:
5141 | # Get project ID
5142 | project_id = await ProjectManager.get_project_id(context)
5143 | if not project_id:
5144 | logger.error("Cannot update project state: no project associated with this conversation")
5145 | return False, None
5146 |
5147 | # Get user information
5148 | current_user_id = await require_current_user(context, "update project state")
5149 | if not current_user_id:
5150 | return False, None
5151 |
5152 | # Get existing project info
5153 | project_info = ProjectStorage.read_project_info(project_id)
5154 | if not project_info:
5155 | logger.error(f"Cannot update project state: no project info found for {project_id}")
5156 | return False, None
5157 |
5158 | # Apply updates
5159 | if state:
5160 | project_info.state = ProjectState(state)
5161 |
5162 | if status_message:
5163 | project_info.status_message = status_message
5164 |
5165 | # Update metadata
5166 | project_info.updated_at = datetime.utcnow()
5167 |
5168 | # Save the project info
5169 | ProjectStorage.write_project_info(project_id, project_info)
5170 |
5171 | # Log the update
5172 | event_type = LogEntryType.STATUS_CHANGED
5173 | message = f"Updated project state to {project_info.state.value}"
5174 |
5175 | await ProjectStorage.log_project_event(
5176 | context=context,
5177 | project_id=project_id,
5178 | entry_type=event_type.value,
5179 | message=message,
5180 | metadata={
5181 | "state": project_info.state.value,
5182 | "status_message": status_message,
5183 | },
5184 | )
5185 |
5186 | # Notify linked conversations
5187 | await ProjectNotifier.notify_project_update(
5188 | context=context,
5189 | project_id=project_id,
5190 | update_type="project_state",
5191 | message=f"Project state updated: {project_info.state.value}",
5192 | )
5193 |
5194 | return True, project_info
5195 |
5196 | except Exception as e:
5197 | logger.exception(f"Error updating project state: {e}")
5198 | return False, None
5199 |
5200 | @staticmethod
5201 | async def get_information_requests(
5202 | context: ConversationContext,
5203 | ) -> List[InformationRequest]:
5204 | """Gets all information requests for the current conversation's project."""
5205 | project_id = await ProjectManager.get_project_id(context)
5206 | if not project_id:
5207 | return []
5208 |
5209 | return ProjectStorage.get_all_information_requests(project_id)
5210 |
5211 | @staticmethod
5212 | async def create_information_request(
5213 | context: ConversationContext,
5214 | title: str,
5215 | description: str,
5216 | priority: RequestPriority = RequestPriority.MEDIUM,
5217 | related_goal_ids: Optional[List[str]] = None,
5218 | ) -> Tuple[bool, Optional[InformationRequest]]:
5219 | """
5220 | Creates a new information request.
5221 |
5222 | Args:
5223 | context: Current conversation context
5224 | title: Title of the request
5225 | description: Description of the request
5226 | priority: Priority level
5227 | related_goal_ids: Optional list of related goal IDs
5228 |
5229 | Returns:
5230 | Tuple of (success, information_request)
5231 | """
5232 | try:
5233 | # Get project ID
5234 | project_id = await ProjectManager.get_project_id(context)
5235 | if not project_id:
5236 | logger.error("Cannot create information request: no project associated with this conversation")
5237 | return False, None
5238 |
5239 | # Get user information
5240 | current_user_id = await require_current_user(context, "create information request")
5241 | if not current_user_id:
5242 | return False, None
5243 |
5244 | # Create the information request
5245 | information_request = InformationRequest(
5246 | title=title,
5247 | description=description,
5248 | priority=priority,
5249 | related_goal_ids=related_goal_ids or [],
5250 | created_by=current_user_id,
5251 | updated_by=current_user_id,
5252 | conversation_id=str(context.id),
5253 | )
5254 |
5255 | # Save the request
5256 | ProjectStorage.write_information_request(project_id, information_request)
5257 |
5258 | # Log the creation
5259 | await ProjectStorage.log_project_event(
5260 | context=context,
5261 | project_id=project_id,
5262 | entry_type=LogEntryType.REQUEST_CREATED.value,
5263 | message=f"Created information request: {title}",
5264 | related_entity_id=information_request.request_id,
5265 | metadata={
5266 | "priority": priority.value,
5267 | "request_id": information_request.request_id,
5268 | },
5269 | )
5270 |
5271 | # For high priority requests, we could update project info or add an indicator
5272 | # in the future if needed
5273 |
5274 | # Notify linked conversations
5275 | await ProjectNotifier.notify_project_update(
5276 | context=context,
5277 | project_id=project_id,
5278 | update_type="information_request",
5279 | message=f"New information request: {title} (Priority: {priority.value})",
5280 | )
5281 |
5282 | # Update all project UI inspectors
5283 | await ProjectStorage.refresh_all_project_uis(context, project_id)
5284 |
5285 | return True, information_request
5286 |
5287 | except Exception as e:
5288 | logger.exception(f"Error creating information request: {e}")
5289 | return False, None
5290 |
5291 | @staticmethod
5292 | async def resolve_information_request(
5293 | context: ConversationContext,
5294 | request_id: str,
5295 | resolution: str,
5296 | ) -> Tuple[bool, Optional[InformationRequest]]:
5297 | """
5298 | Resolves an information request.
5299 |
5300 | Args:
5301 | context: Current conversation context
5302 | request_id: ID of the request to resolve
5303 | resolution: Resolution information
5304 |
5305 | Returns:
5306 | Tuple of (success, information_request)
5307 | """
5308 | try:
5309 | # Get project ID
5310 | project_id = await ProjectManager.get_project_id(context)
5311 | if not project_id:
5312 | logger.error("Cannot resolve information request: no project associated with this conversation")
5313 | return False, None
5314 |
5315 | # Get user information
5316 | current_user_id = await require_current_user(context, "resolve information request")
5317 | if not current_user_id:
5318 | return False, None
5319 |
5320 | # Get the information request
5321 | information_request = ProjectStorage.read_information_request(project_id, request_id)
5322 | if not information_request:
5323 | # Try to find it in all requests
5324 | all_requests = ProjectStorage.get_all_information_requests(project_id)
5325 | for request in all_requests:
5326 | if request.request_id == request_id:
5327 | information_request = request
5328 | break
5329 |
5330 | if not information_request:
5331 | logger.error(f"Information request {request_id} not found")
5332 | return False, None
5333 |
5334 | # Check if already resolved
5335 | if information_request.status == RequestStatus.RESOLVED:
5336 | logger.warning(f"Information request {request_id} is already resolved")
5337 | return True, information_request
5338 |
5339 | # Update the request
5340 | information_request.status = RequestStatus.RESOLVED
5341 | information_request.resolution = resolution
5342 | information_request.resolved_at = datetime.utcnow()
5343 | information_request.resolved_by = current_user_id
5344 |
5345 | # Add to history
5346 | information_request.updates.append({
5347 | "timestamp": datetime.utcnow().isoformat(),
5348 | "user_id": current_user_id,
5349 | "message": f"Request resolved: {resolution}",
5350 | "status": RequestStatus.RESOLVED.value,
5351 | })
5352 |
5353 | # Update metadata
5354 | information_request.updated_at = datetime.utcnow()
5355 | information_request.updated_by = current_user_id
5356 | information_request.version += 1
5357 |
5358 | # Save the updated request
5359 | ProjectStorage.write_information_request(project_id, information_request)
5360 |
5361 | # Log the resolution
5362 | await ProjectStorage.log_project_event(
5363 | context=context,
5364 | project_id=project_id,
5365 | entry_type=LogEntryType.REQUEST_RESOLVED.value,
5366 | message=f"Resolved information request: {information_request.title}",
5367 | related_entity_id=information_request.request_id,
5368 | metadata={
5369 | "resolution": resolution,
5370 | "request_title": information_request.title,
5371 | "request_priority": information_request.priority.value
5372 | if hasattr(information_request.priority, "value")
5373 | else information_request.priority,
5374 | },
5375 | )
5376 |
5377 | # High priority request has been resolved, could update project info
5378 | # in the future if needed
5379 |
5380 | # Notify linked conversations
5381 | await ProjectNotifier.notify_project_update(
5382 | context=context,
5383 | project_id=project_id,
5384 | update_type="information_request_resolved",
5385 | message=f"Information request resolved: {information_request.title}",
5386 | )
5387 |
5388 | # Send direct notification to requestor's conversation
5389 | if information_request.conversation_id != str(context.id):
5390 | try:
5391 | # Get client for requestor's conversation
5392 | client = ConversationClientManager.get_conversation_client(
5393 | context, information_request.conversation_id
5394 | )
5395 |
5396 | # Send notification message
5397 | await client.send_messages(
5398 | NewConversationMessage(
5399 | content=f"Coordinator has resolved your request '{information_request.title}': {resolution}",
5400 | message_type=MessageType.notice,
5401 | )
5402 | )
5403 | except Exception as e:
5404 | logger.warning(f"Could not send notification to requestor: {e}")
5405 |
5406 | # Update all project UI inspectors
5407 | await ProjectStorage.refresh_all_project_uis(context, project_id)
5408 |
5409 | return True, information_request
5410 |
5411 | except Exception as e:
5412 | logger.exception(f"Error resolving information request: {e}")
5413 | return False, None
5414 |
5415 | @staticmethod
5416 | async def get_project_log(context: ConversationContext) -> Optional[ProjectLog]:
5417 | """Gets the project log for the current conversation's project."""
5418 | project_id = await ProjectManager.get_project_id(context)
5419 | if not project_id:
5420 | return None
5421 |
5422 | return ProjectStorage.read_project_log(project_id)
5423 |
5424 | @staticmethod
5425 | async def get_project(context: ConversationContext) -> Optional[Project]:
5426 | """Gets the project information for the current conversation's project."""
5427 | project_id = await ProjectManager.get_project_id(context)
5428 | if not project_id:
5429 | return None
5430 | project = Project(
5431 | info=ProjectStorage.read_project_info(project_id),
5432 | brief=ProjectStorage.read_project_brief(project_id),
5433 | whiteboard=ProjectStorage.read_project_whiteboard(project_id),
5434 | requests=ProjectStorage.get_all_information_requests(project_id),
5435 | log=ProjectStorage.read_project_log(project_id),
5436 | )
5437 | return project
5438 |
5439 | @staticmethod
5440 | async def get_project_info(context: ConversationContext, project_id: Optional[str] = None) -> Optional[ProjectInfo]:
5441 | """
5442 | Gets the project information including share URL and team conversation details.
5443 |
5444 | Args:
5445 | context: Current conversation context
5446 | project_id: Optional project ID (if not provided, will be retrieved from context)
5447 |
5448 | Returns:
5449 | ProjectInfo object or None if not found
5450 | """
5451 | try:
5452 | # Get project ID if not provided
5453 | if not project_id:
5454 | project_id = await ProjectManager.get_project_id(context)
5455 | if not project_id:
5456 | return None
5457 |
5458 | # Read project info
5459 | project_info = ProjectStorage.read_project_info(project_id)
5460 | return project_info
5461 |
5462 | except Exception as e:
5463 | logger.exception(f"Error getting project info: {e}")
5464 | return None
5465 |
5466 | @staticmethod
5467 | async def get_project_whiteboard(
5468 | context: ConversationContext,
5469 | ) -> Optional[ProjectWhiteboard]:
5470 | """Gets the project whiteboard for the current conversation's project."""
5471 | project_id = await ProjectManager.get_project_id(context)
5472 | if not project_id:
5473 | return None
5474 |
5475 | return ProjectStorage.read_project_whiteboard(project_id)
5476 |
5477 | @staticmethod
5478 | async def update_whiteboard(
5479 | context: ConversationContext,
5480 | content: str,
5481 | is_auto_generated: bool = True,
5482 | send_notification: bool = False, # Add parameter to control notifications
5483 | ) -> Tuple[bool, Optional[ProjectWhiteboard]]:
5484 | """
5485 | Updates the project whiteboard content.
5486 |
5487 | Args:
5488 | context: Current conversation context
5489 | content: Whiteboard content in markdown format
5490 | is_auto_generated: Whether the content was automatically generated
5491 | send_notification: Whether to send notifications about the update (default: False)
5492 |
5493 | Returns:
5494 | Tuple of (success, project_kb)
5495 | """
5496 | try:
5497 | # Get project ID
5498 | project_id = await ProjectManager.get_project_id(context)
5499 | if not project_id:
5500 | logger.error("Cannot update whiteboard: no project associated with this conversation")
5501 | return False, None
5502 |
5503 | # Get user information
5504 | current_user_id = await require_current_user(context, "update whiteboard")
5505 | if not current_user_id:
5506 | return False, None
5507 |
5508 | # Get existing whiteboard or create new one
5509 | whiteboard = ProjectStorage.read_project_whiteboard(project_id)
5510 | is_new = False
5511 |
5512 | if not whiteboard:
5513 | whiteboard = ProjectWhiteboard(
5514 | created_by=current_user_id,
5515 | updated_by=current_user_id,
5516 | conversation_id=str(context.id),
5517 | content="",
5518 | )
5519 | is_new = True
5520 |
5521 | # Update the content
5522 | whiteboard.content = content
5523 | whiteboard.is_auto_generated = is_auto_generated
5524 |
5525 | # Update metadata
5526 | whiteboard.updated_at = datetime.utcnow()
5527 | whiteboard.updated_by = current_user_id
5528 | whiteboard.version += 1
5529 |
5530 | # Save the whiteboard
5531 | ProjectStorage.write_project_whiteboard(project_id, whiteboard)
5532 |
5533 | # Log the update
5534 | event_type = LogEntryType.KB_UPDATE
5535 | update_type = "auto-generated" if is_auto_generated else "manual"
5536 | message = f"{'Created' if is_new else 'Updated'} project whiteboard ({update_type})"
5537 |
5538 | await ProjectStorage.log_project_event(
5539 | context=context,
5540 | project_id=project_id,
5541 | entry_type=event_type.value,
5542 | message=message,
5543 | )
5544 |
5545 | # Only notify linked conversations if explicitly requested
5546 | # This prevents auto-updates from generating notifications
5547 | if send_notification:
5548 | await ProjectNotifier.notify_project_update(
5549 | context=context,
5550 | project_id=project_id,
5551 | update_type="project_whiteboard",
5552 | message="Project whiteboard updated",
5553 | )
5554 | else:
5555 | # Just refresh the UI without sending notifications
5556 | await ProjectStorage.refresh_all_project_uis(context, project_id)
5557 |
5558 | return True, whiteboard
5559 |
5560 | except Exception as e:
5561 | logger.exception(f"Error updating whiteboard: {e}")
5562 | return False, None
5563 |
5564 | @staticmethod
5565 | async def auto_update_whiteboard(
5566 | context: ConversationContext,
5567 | ) -> Tuple[bool, Optional[ProjectWhiteboard]]:
5568 | """
5569 | Automatically updates the whiteboard by analyzing chat history.
5570 |
5571 | This method:
5572 | 1. Retrieves recent conversation messages
5573 | 2. Sends them to the LLM with a prompt to extract important info
5574 | 3. Updates the whiteboard with the extracted content
5575 |
5576 | Args:
5577 | context: Current conversation context
5578 | chat_history: Recent chat messages to analyze
5579 |
5580 | Returns:
5581 | Tuple of (success, project_kb)
5582 | """
5583 | try:
5584 | messages = await context.get_messages()
5585 | chat_history = messages.messages
5586 |
5587 | # Get project ID
5588 | project_id = await ProjectManager.get_project_id(context)
5589 | if not project_id:
5590 | logger.error("Cannot auto-update whiteboard: no project associated with this conversation")
5591 | return False, None
5592 |
5593 | # Get user information for storage purposes
5594 | current_user_id = await require_current_user(context, "auto-update whiteboard")
5595 | if not current_user_id:
5596 | return False, None
5597 |
5598 | # Skip if no messages to analyze
5599 | if not chat_history:
5600 | logger.warning("No chat history to analyze for whiteboard update")
5601 | return False, None
5602 |
5603 | # Format the chat history for the prompt
5604 | chat_history_text = ""
5605 | for msg in chat_history:
5606 | sender_type = (
5607 | "User" if msg.sender and msg.sender.participant_role == ParticipantRole.user else "Assistant"
5608 | )
5609 | chat_history_text += f"{sender_type}: {msg.content}\n\n"
5610 |
5611 | # Get config for the LLM call
5612 | config = await assistant_config.get(context.assistant)
5613 |
5614 | # Construct the whiteboard prompt with the chat history
5615 | whiteboard_prompt = f"""
5616 | {config.prompt_config.whiteboard_prompt}
5617 |
5618 | <CHAT_HISTORY>
5619 | {chat_history_text}
5620 | </CHAT_HISTORY>
5621 | """
5622 |
5623 | # Create a completion with the whiteboard prompt
5624 | async with openai_client.create_client(config.service_config, api_version="2024-06-01") as client:
5625 | completion = await client.chat.completions.create(
5626 | model=config.request_config.openai_model,
5627 | messages=[{"role": "user", "content": whiteboard_prompt}],
5628 | max_tokens=2500, # Limiting to 2500 tokens to keep whiteboard content manageable
5629 | )
5630 |
5631 | # Extract the content from the completion
5632 | content = completion.choices[0].message.content or ""
5633 |
5634 | # Extract just the whiteboard content
5635 | whiteboard_content = ""
5636 |
5637 | # Look for content between <WHITEBOARD> tags
5638 | match = re.search(r"<WHITEBOARD>(.*?)</WHITEBOARD>", content, re.DOTALL)
5639 | if match:
5640 | whiteboard_content = match.group(1).strip()
5641 | else:
5642 | # If no tags, use the whole content
5643 | whiteboard_content = content.strip()
5644 |
5645 | # Only update if we have content
5646 | if not whiteboard_content:
5647 | logger.warning("No content extracted from whiteboard LLM analysis")
5648 | return False, None
5649 |
5650 | # Update the whiteboard with the extracted content
5651 | # Use send_notification=False to avoid sending notifications for automatic updates
5652 | return await ProjectManager.update_whiteboard(
5653 | context=context,
5654 | content=whiteboard_content,
5655 | is_auto_generated=True,
5656 | send_notification=False,
5657 | )
5658 |
5659 | except Exception as e:
5660 | logger.exception(f"Error auto-updating whiteboard: {e}")
5661 | return False, None
5662 |
5663 | @staticmethod
5664 | async def complete_project(
5665 | context: ConversationContext,
5666 | summary: Optional[str] = None,
5667 | ) -> Tuple[bool, Optional[ProjectInfo]]:
5668 | """
5669 | Completes a project and updates the project state.
5670 |
5671 | Args:
5672 | context: Current conversation context
5673 | summary: Optional summary of project results
5674 |
5675 | Returns:
5676 | Tuple of (success, project_info)
5677 | """
5678 | try:
5679 | # Get project ID
5680 | project_id = await ProjectManager.get_project_id(context)
5681 | if not project_id:
5682 | logger.error("Cannot complete project: no project associated with this conversation")
5683 | return False, None
5684 |
5685 | # Get role - only Coordinator can complete a project
5686 | role = await ProjectManager.get_project_role(context)
5687 | if role != ConversationRole.COORDINATOR:
5688 | logger.error("Only Coordinator can complete a project")
5689 | return False, None
5690 |
5691 | # Update project state to completed
5692 | status_message = summary if summary else "Project completed successfully"
5693 | success, project_info = await ProjectManager.update_project_state(
5694 | context=context,
5695 | state=ProjectState.COMPLETED.value,
5696 | status_message=status_message,
5697 | )
5698 |
5699 | if not success or not project_info:
5700 | return False, None
5701 |
5702 | # Add completion entry to the log
5703 | await ProjectStorage.log_project_event(
5704 | context=context,
5705 | project_id=project_id,
5706 | entry_type=LogEntryType.PROJECT_COMPLETED.value,
5707 | message=f"Project completed: {status_message}",
5708 | )
5709 |
5710 | # Notify linked conversations with emphasis
5711 | await ProjectNotifier.notify_project_update(
5712 | context=context,
5713 | project_id=project_id,
5714 | update_type="project_completed",
5715 | message=f"🎉 PROJECT COMPLETED: {status_message}",
5716 | )
5717 |
5718 | return True, project_info
5719 |
5720 | except Exception as e:
5721 | logger.exception(f"Error completing project: {e}")
5722 | return False, None
5723 |
5724 |
5725 | === File: assistants/project-assistant/assistant/project_notifications.py ===
5726 | """
5727 | Project notification and UI refresh functionality.
5728 |
5729 | This module handles notifications between conversations and UI refresh events
5730 | for the project assistant, ensuring all participants stay in sync.
5731 | """
5732 |
5733 | from typing import Any, Dict, Optional
5734 |
5735 | from semantic_workbench_api_model.workbench_model import AssistantStateEvent, MessageType, NewConversationMessage
5736 | from semantic_workbench_assistant.assistant_app import ConversationContext
5737 |
5738 | from .logging import logger
5739 | from .project_storage import ProjectStorage
5740 |
5741 |
5742 | class ProjectNotifier:
5743 | """Handles notifications between conversations for project updates."""
5744 |
5745 | @staticmethod
5746 | async def send_notice_to_linked_conversations(context: ConversationContext, project_id: str, message: str) -> None:
5747 | """
5748 | Sends a notice message to all linked conversations except:
5749 | 1. The current conversation
5750 | 2. The shareable team conversation template (used only for creating the share URL)
5751 |
5752 | NOTE: The shareable team conversation is NEVER used directly by any user.
5753 | It's just a template that gets copied when team members redeem the share URL
5754 | to create their own individual team conversations. We exclude it from notifications
5755 | because no one will ever see those notifications.
5756 |
5757 | This method does NOT refresh any UI inspector panels.
5758 |
5759 | Args:
5760 | context: Current conversation context
5761 | project_id: ID of the project
5762 | message: Notification message to send
5763 | """
5764 | # Import ConversationClientManager locally to avoid circular imports
5765 | from .conversation_clients import ConversationClientManager
5766 | from .conversation_project_link import ConversationProjectManager
5767 |
5768 | # Get conversation IDs in the same project
5769 | linked_conversations = await ConversationProjectManager.get_linked_conversations(context)
5770 | current_conversation_id = str(context.id)
5771 |
5772 | # Get the shareable team conversation ID from project info
5773 | # This is the conversation created by the coordinator for sharing,
5774 | # not an actual user conversation
5775 | shareable_conversation_id = None
5776 | project_info = ProjectStorage.read_project_info(project_id)
5777 | if project_info and project_info.team_conversation_id:
5778 | shareable_conversation_id = project_info.team_conversation_id
5779 |
5780 | # Send notification to each linked conversation, excluding current and shareable conversation
5781 | for conv_id in linked_conversations:
5782 | # Skip current conversation and the shareable team conversation
5783 | if conv_id != current_conversation_id and (
5784 | not shareable_conversation_id or conv_id != shareable_conversation_id
5785 | ):
5786 | try:
5787 | # Get client for the target conversation
5788 | client = ConversationClientManager.get_conversation_client(context, conv_id)
5789 |
5790 | # Send the notification
5791 | await client.send_messages(
5792 | NewConversationMessage(
5793 | content=message,
5794 | message_type=MessageType.notice,
5795 | metadata={
5796 | "debug": {
5797 | "project_id": project_id,
5798 | "message": message,
5799 | "sender": str(context.id),
5800 | }
5801 | },
5802 | )
5803 | )
5804 | logger.debug(f"Sent notification to conversation {conv_id}")
5805 | except Exception as e:
5806 | logger.error(f"Failed to notify conversation {conv_id}: {e}")
5807 |
5808 | @staticmethod
5809 | async def notify_project_update(
5810 | context: ConversationContext,
5811 | project_id: str,
5812 | update_type: str,
5813 | message: str,
5814 | data: Optional[Dict[str, Any]] = None,
5815 | send_notification: bool = True, # Add parameter to control notifications
5816 | ) -> None:
5817 | """
5818 | Complete project update: sends notices to all conversations and refreshes all UI inspector panels.
5819 |
5820 | This method:
5821 | 1. Sends a notice message to the current conversation (if send_notification=True)
5822 | 2. Sends the same notice message to all linked conversations (if send_notification=True)
5823 | 3. Refreshes UI inspector panels for all conversations in the project
5824 |
5825 | Use this for important project updates that need both user notification AND UI refresh.
5826 | Set send_notification=False for frequent updates (like file syncs, whiteboard updates) to
5827 | avoid notification spam.
5828 |
5829 | Args:
5830 | context: Current conversation context
5831 | project_id: ID of the project
5832 | update_type: Type of update (e.g., 'brief', 'project_info', 'information_request', etc.)
5833 | message: Notification message to display to users
5834 | data: Optional additional data related to the update
5835 | send_notification: Whether to send notifications (default: True)
5836 | """
5837 |
5838 | # Only send notifications if explicitly requested
5839 | if send_notification:
5840 | # Notify all linked conversations with the same message
5841 | await ProjectNotifier.send_notice_to_linked_conversations(context, project_id, message)
5842 |
5843 | # Always refresh all project UI inspector panels to keep UI in sync
5844 | # This will update the UI without sending notifications
5845 | await ProjectStorage.refresh_all_project_uis(context, project_id)
5846 |
5847 |
5848 | async def refresh_current_ui(context: ConversationContext) -> None:
5849 | """
5850 | Refreshes only the current conversation's UI inspector panel.
5851 |
5852 | Use this when a change only affects the local conversation's view
5853 | and doesn't need to be synchronized with other conversations.
5854 | """
5855 |
5856 | # Create the state event
5857 | state_event = AssistantStateEvent(
5858 | state_id="project_status", # Must match the inspector_state_providers key in chat.py
5859 | event="updated",
5860 | state=None,
5861 | )
5862 |
5863 | # Send the event to the current context
5864 | await context.send_conversation_state_event(state_event)
5865 |
5866 |
5867 | async def refresh_all_project_uis(context: ConversationContext, project_id: str) -> None:
5868 | """
5869 | Refreshes the UI inspector panels of all conversations in a project except the
5870 | shareable team conversation template.
5871 |
5872 | There are three types of conversations in the system:
5873 | 1. Coordinator Conversation - The main conversation for the project owner
5874 | 2. Shareable Team Conversation Template - Only used to generate the share URL, never directly used by any user
5875 | 3. Team Conversation(s) - Individual conversations for each team member
5876 |
5877 | This sends a state event to all relevant conversations (Coordinator and all active team members)
5878 | involved in the project to refresh their inspector panels, ensuring all
5879 | participants have the latest information without sending any text notifications.
5880 |
5881 | The shareable team conversation template is excluded because no user will ever see it -
5882 | it only exists to create the share URL that team members can use to join.
5883 |
5884 | Use this when project data has changed and all UIs need to be updated,
5885 | but you don't want to send notification messages to users.
5886 |
5887 | Args:
5888 | context: Current conversation context
5889 | project_id: The project ID
5890 | """
5891 | # Import ConversationClientManager locally to avoid circular imports
5892 | from .conversation_clients import ConversationClientManager
5893 | from .conversation_project_link import ConversationProjectManager
5894 |
5895 | try:
5896 | # First update the current conversation's UI
5897 | await refresh_current_ui(context)
5898 |
5899 | # Get the shareable team conversation ID from project info to exclude it
5900 | shareable_conversation_id = None
5901 | project_info = ProjectStorage.read_project_info(project_id)
5902 | if project_info and project_info.team_conversation_id:
5903 | shareable_conversation_id = project_info.team_conversation_id
5904 |
5905 | # Get Coordinator client and update Coordinator if not the current conversation
5906 | (
5907 | coordinator_client,
5908 | coordinator_conversation_id,
5909 | ) = await ConversationClientManager.get_coordinator_client_for_project(context, project_id)
5910 | if coordinator_client and coordinator_conversation_id:
5911 | try:
5912 | state_event = AssistantStateEvent(state_id="project_status", event="updated", state=None)
5913 | # Get assistant ID from context
5914 | assistant_id = context.assistant.id
5915 | await coordinator_client.send_conversation_state_event(assistant_id, state_event)
5916 | logger.debug(
5917 | f"Sent state event to Coordinator conversation {coordinator_conversation_id} to refresh inspector"
5918 | )
5919 | except Exception as e:
5920 | logger.warning(f"Error sending state event to Coordinator: {e}")
5921 |
5922 | # Get all team conversation clients and update them
5923 | linked_conversations = await ConversationProjectManager.get_linked_conversations(context)
5924 | current_id = str(context.id)
5925 |
5926 | for conv_id in linked_conversations:
5927 | # Skip current conversation, coordinator conversation, and shareable conversation
5928 | if (
5929 | conv_id != current_id
5930 | and (not coordinator_conversation_id or conv_id != coordinator_conversation_id)
5931 | and (not shareable_conversation_id or conv_id != shareable_conversation_id)
5932 | ):
5933 | try:
5934 | # Get client for the conversation
5935 | client = ConversationClientManager.get_conversation_client(context, conv_id)
5936 |
5937 | # Send state event to refresh the inspector panel
5938 | state_event = AssistantStateEvent(state_id="project_status", event="updated", state=None)
5939 | # Get assistant ID from context
5940 | assistant_id = context.assistant.id
5941 | await client.send_conversation_state_event(assistant_id, state_event)
5942 | except Exception as e:
5943 | logger.warning(f"Error sending state event to conversation {conv_id}: {e}")
5944 | continue
5945 | elif conv_id == shareable_conversation_id:
5946 | logger.info(f"Skipping UI update for shareable conversation: {conv_id}")
5947 |
5948 | except Exception as e:
5949 | logger.warning(f"Error notifying all project UIs: {e}")
5950 |
5951 |
5952 | === File: assistants/project-assistant/assistant/project_storage.py ===
5953 | """
5954 | Project storage management module.
5955 |
5956 | Provides direct access to project data with a clean, simple storage approach.
5957 | """
5958 |
5959 | import pathlib
5960 | from datetime import datetime
5961 | from typing import Any, Dict, List, Optional
5962 |
5963 | from semantic_workbench_assistant import settings
5964 | from semantic_workbench_assistant.assistant_app import ConversationContext
5965 | from semantic_workbench_assistant.assistant_app.context import storage_directory_for_context
5966 | from semantic_workbench_assistant.storage import read_model, write_model
5967 |
5968 | # Import inside functions to avoid circular imports
5969 | from .project_data import (
5970 | InformationRequest,
5971 | LogEntry,
5972 | LogEntryType,
5973 | Project,
5974 | ProjectBrief,
5975 | ProjectInfo,
5976 | ProjectLog,
5977 | ProjectWhiteboard,
5978 | )
5979 | from .project_storage_models import CoordinatorConversationMessage, CoordinatorConversationStorage
5980 | from .utils import get_current_user
5981 |
5982 |
5983 | class ProjectStorageManager:
5984 | """Manages storage paths and access for project data."""
5985 |
5986 | PROJECTS_ROOT = "projects"
5987 |
5988 | # File names for project entities
5989 | PROJECT_INFO_FILE = "project.json"
5990 | PROJECT_BRIEF_FILE = "brief.json"
5991 | PROJECT_LOG_FILE = "log.json"
5992 | PROJECT_WHITEBOARD_FILE = "whiteboard.json"
5993 | COORDINATOR_CONVERSATION_FILE = "coordinator_conversation.json"
5994 | PROJECT_FILE = "project_data.json"
5995 |
5996 | @staticmethod
5997 | def get_projects_root() -> pathlib.Path:
5998 | """Gets the root path for all projects."""
5999 | return pathlib.Path(settings.storage.root) / ProjectStorageManager.PROJECTS_ROOT
6000 |
6001 | @staticmethod
6002 | def get_project_dir(project_id: str) -> pathlib.Path:
6003 | """Gets the directory for a specific project."""
6004 | projects_root = ProjectStorageManager.get_projects_root()
6005 | project_dir = projects_root / project_id
6006 | project_dir.mkdir(parents=True, exist_ok=True)
6007 | return project_dir
6008 |
6009 | @staticmethod
6010 | def get_linked_conversations_dir(project_id: str) -> pathlib.Path:
6011 | """Gets the directory that tracks linked conversations for a project."""
6012 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6013 | linked_dir = project_dir / "linked_conversations"
6014 | linked_dir.mkdir(parents=True, exist_ok=True)
6015 | return linked_dir
6016 |
6017 | @staticmethod
6018 | def get_project_info_path(project_id: str) -> pathlib.Path:
6019 | """Gets the path to the project info file."""
6020 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6021 | return project_dir / ProjectStorageManager.PROJECT_INFO_FILE
6022 |
6023 | @staticmethod
6024 | def get_brief_path(project_id: str) -> pathlib.Path:
6025 | """Gets the path to the project brief file."""
6026 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6027 | return project_dir / ProjectStorageManager.PROJECT_BRIEF_FILE
6028 |
6029 | @staticmethod
6030 | def get_project_log_path(project_id: str) -> pathlib.Path:
6031 | """Gets the path to the project log file."""
6032 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6033 | return project_dir / ProjectStorageManager.PROJECT_LOG_FILE
6034 |
6035 | @staticmethod
6036 | def get_project_whiteboard_path(project_id: str) -> pathlib.Path:
6037 | """Gets the path to the project whiteboard file."""
6038 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6039 | return project_dir / ProjectStorageManager.PROJECT_WHITEBOARD_FILE
6040 |
6041 | @staticmethod
6042 | def get_coordinator_conversation_path(project_id: str) -> pathlib.Path:
6043 | """Gets the path to the Coordinator conversation file."""
6044 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6045 | return project_dir / ProjectStorageManager.COORDINATOR_CONVERSATION_FILE
6046 |
6047 | @staticmethod
6048 | def get_project_path(project_id: str) -> pathlib.Path:
6049 | """Gets the path to the complete Project data file."""
6050 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6051 | return project_dir / ProjectStorageManager.PROJECT_FILE
6052 |
6053 | @staticmethod
6054 | def get_information_requests_dir(project_id: str) -> pathlib.Path:
6055 | """Gets the directory containing all information requests."""
6056 | project_dir = ProjectStorageManager.get_project_dir(project_id)
6057 | requests_dir = project_dir / "requests"
6058 | requests_dir.mkdir(parents=True, exist_ok=True)
6059 | return requests_dir
6060 |
6061 | @staticmethod
6062 | def get_information_request_path(project_id: str, request_id: str) -> pathlib.Path:
6063 | """Gets the path to an information request file."""
6064 | requests_dir = ProjectStorageManager.get_information_requests_dir(project_id)
6065 | return requests_dir / f"{request_id}.json"
6066 |
6067 | @staticmethod
6068 | def project_exists(project_id: str) -> bool:
6069 | """Checks if a project exists."""
6070 | project_dir = ProjectStorageManager.get_projects_root() / project_id
6071 | return project_dir.exists()
6072 |
6073 | @staticmethod
6074 | def get_conversation_role_file_path(context: ConversationContext) -> pathlib.Path:
6075 | """Gets the path to the file that stores a conversation's role in projects."""
6076 | storage_dir = storage_directory_for_context(context)
6077 | storage_dir.mkdir(parents=True, exist_ok=True)
6078 | return storage_dir / "project_role.json"
6079 |
6080 | @staticmethod
6081 | def get_conversation_project_file_path(context: ConversationContext) -> pathlib.Path:
6082 | """Gets the path to the file that stores a conversation's project association."""
6083 | storage_dir = storage_directory_for_context(context)
6084 | storage_dir.mkdir(parents=True, exist_ok=True)
6085 | file_path = storage_dir / "project_association.json"
6086 | return file_path
6087 |
6088 |
6089 | class ProjectStorage:
6090 | """Unified storage operations for project data."""
6091 |
6092 | @staticmethod
6093 | def read_project_info(project_id: str) -> Optional[ProjectInfo]:
6094 | """Reads the project info."""
6095 | path = ProjectStorageManager.get_project_info_path(project_id)
6096 | return read_model(path, ProjectInfo)
6097 |
6098 | @staticmethod
6099 | def write_project_info(project_id: str, info: ProjectInfo) -> pathlib.Path:
6100 | """Writes the project info."""
6101 | path = ProjectStorageManager.get_project_info_path(project_id)
6102 | write_model(path, info)
6103 | return path
6104 |
6105 | @staticmethod
6106 | def read_project_brief(project_id: str) -> Optional[ProjectBrief]:
6107 | """Reads the project brief."""
6108 | path = ProjectStorageManager.get_brief_path(project_id)
6109 | return read_model(path, ProjectBrief)
6110 |
6111 | @staticmethod
6112 | def write_project_brief(project_id: str, brief: ProjectBrief) -> pathlib.Path:
6113 | """Writes the project brief."""
6114 | path = ProjectStorageManager.get_brief_path(project_id)
6115 | write_model(path, brief)
6116 | return path
6117 |
6118 | @staticmethod
6119 | def read_project_log(project_id: str) -> Optional[ProjectLog]:
6120 | """Reads the project log."""
6121 | path = ProjectStorageManager.get_project_log_path(project_id)
6122 | return read_model(path, ProjectLog)
6123 |
6124 | @staticmethod
6125 | def write_project_log(project_id: str, log: ProjectLog) -> pathlib.Path:
6126 | """Writes the project log."""
6127 | path = ProjectStorageManager.get_project_log_path(project_id)
6128 | write_model(path, log)
6129 | return path
6130 |
6131 | @staticmethod
6132 | def read_project_whiteboard(project_id: str) -> Optional[ProjectWhiteboard]:
6133 | """Reads the project whiteboard."""
6134 | path = ProjectStorageManager.get_project_whiteboard_path(project_id)
6135 | return read_model(path, ProjectWhiteboard)
6136 |
6137 | @staticmethod
6138 | def read_coordinator_conversation(project_id: str) -> Optional[CoordinatorConversationStorage]:
6139 | """Reads the Coordinator conversation messages for a project."""
6140 | path = ProjectStorageManager.get_coordinator_conversation_path(project_id)
6141 | return read_model(path, CoordinatorConversationStorage)
6142 |
6143 | @staticmethod
6144 | def write_coordinator_conversation(project_id: str, conversation: CoordinatorConversationStorage) -> pathlib.Path:
6145 | """Writes the Coordinator conversation messages to storage."""
6146 | path = ProjectStorageManager.get_coordinator_conversation_path(project_id)
6147 | write_model(path, conversation)
6148 | return path
6149 |
6150 | @staticmethod
6151 | def append_coordinator_message(
6152 | project_id: str,
6153 | message_id: str,
6154 | content: str,
6155 | sender_name: str,
6156 | is_assistant: bool = False,
6157 | timestamp: Optional[datetime] = None,
6158 | ) -> None:
6159 | """
6160 | Appends a message to the Coordinator conversation storage.
6161 |
6162 | Args:
6163 | project_id: The ID of the project
6164 | message_id: The ID of the message
6165 | content: The message content
6166 | sender_name: The name of the sender
6167 | is_assistant: Whether the message is from the assistant
6168 | timestamp: The timestamp of the message (defaults to now)
6169 | """
6170 | # Get existing conversation or create new one
6171 | conversation = ProjectStorage.read_coordinator_conversation(project_id)
6172 | if not conversation:
6173 | conversation = CoordinatorConversationStorage(project_id=project_id)
6174 |
6175 | # Create new message
6176 | new_message = CoordinatorConversationMessage(
6177 | message_id=message_id,
6178 | content=content,
6179 | sender_name=sender_name,
6180 | timestamp=timestamp or datetime.utcnow(),
6181 | is_assistant=is_assistant,
6182 | )
6183 |
6184 | # Add to conversation (only keep most recent 50 messages)
6185 | conversation.messages.append(new_message)
6186 | if len(conversation.messages) > 50:
6187 | conversation.messages = conversation.messages[-50:]
6188 |
6189 | conversation.last_updated = datetime.utcnow()
6190 |
6191 | # Save the updated conversation
6192 | ProjectStorage.write_coordinator_conversation(project_id, conversation)
6193 |
6194 | @staticmethod
6195 | def write_project_whiteboard(project_id: str, whiteboard: ProjectWhiteboard) -> pathlib.Path:
6196 | """Writes the project whiteboard."""
6197 | path = ProjectStorageManager.get_project_whiteboard_path(project_id)
6198 | write_model(path, whiteboard)
6199 | return path
6200 |
6201 | @staticmethod
6202 | def read_information_request(project_id: str, request_id: str) -> Optional[InformationRequest]:
6203 | """Reads an information request."""
6204 | path = ProjectStorageManager.get_information_request_path(project_id, request_id)
6205 | return read_model(path, InformationRequest)
6206 |
6207 | @staticmethod
6208 | def write_information_request(project_id: str, request: InformationRequest) -> pathlib.Path:
6209 | """Writes an information request."""
6210 | # Information requests must have an ID
6211 | if not request.request_id:
6212 | raise ValueError("Information request must have a request_id")
6213 |
6214 | path = ProjectStorageManager.get_information_request_path(project_id, request.request_id)
6215 | write_model(path, request)
6216 | return path
6217 |
6218 | @staticmethod
6219 | def read_project(project_id: str) -> Optional[Project]:
6220 | """Reads the complete Project data."""
6221 | path = ProjectStorageManager.get_project_path(project_id)
6222 | return read_model(path, Project)
6223 |
6224 | @staticmethod
6225 | def write_project(project_id: str, project: Project) -> pathlib.Path:
6226 | """Writes the complete Project data."""
6227 | path = ProjectStorageManager.get_project_path(project_id)
6228 | write_model(path, project)
6229 | return path
6230 |
6231 | @staticmethod
6232 | def get_all_information_requests(project_id: str) -> List[InformationRequest]:
6233 | """Gets all information requests for a project."""
6234 | dir_path = ProjectStorageManager.get_information_requests_dir(project_id)
6235 | requests = []
6236 |
6237 | if not dir_path.exists():
6238 | return requests
6239 |
6240 | for file_path in dir_path.glob("*.json"):
6241 | request = read_model(file_path, InformationRequest)
6242 | if request:
6243 | requests.append(request)
6244 |
6245 | # Sort by updated_at timestamp, newest first
6246 | requests.sort(key=lambda r: r.updated_at, reverse=True)
6247 | return requests
6248 |
6249 | @staticmethod
6250 | async def refresh_current_ui(context: ConversationContext) -> None:
6251 | """
6252 | Refreshes only the current conversation's UI inspector panel.
6253 |
6254 | This function is now a wrapper that calls the implementation in project_notifications.py.
6255 | """
6256 | from .project_notifications import refresh_current_ui
6257 |
6258 | await refresh_current_ui(context)
6259 |
6260 | @staticmethod
6261 | async def refresh_all_project_uis(context: ConversationContext, project_id: str) -> None:
6262 | """
6263 | Refreshes the UI inspector panels of all conversations in a project.
6264 |
6265 | This function is now a wrapper that calls the implementation in project_notifications.py.
6266 | """
6267 | from .project_notifications import refresh_all_project_uis
6268 |
6269 | await refresh_all_project_uis(context, project_id)
6270 |
6271 | @staticmethod
6272 | async def log_project_event(
6273 | context: ConversationContext,
6274 | project_id: str,
6275 | entry_type: str,
6276 | message: str,
6277 | related_entity_id: Optional[str] = None,
6278 | metadata: Optional[Dict[str, Any]] = None,
6279 | ) -> bool:
6280 | """
6281 | Logs an event to the project log.
6282 |
6283 | Args:
6284 | context: Current conversation context
6285 | project_id: ID of the project
6286 | entry_type: Type of log entry
6287 | message: Log message
6288 | related_entity_id: Optional ID of a related entity (e.g., information request)
6289 | metadata: Optional additional metadata
6290 |
6291 | Returns:
6292 | True if the log entry was added successfully, False otherwise
6293 | """
6294 | # Get user information
6295 | user_id, user_name = await get_current_user(context)
6296 |
6297 | if not user_id:
6298 | return False
6299 |
6300 | # Default user name if none found
6301 | user_name = user_name or "Unknown User"
6302 |
6303 | # Create a log entry
6304 | entry = LogEntry(
6305 | entry_type=LogEntryType(entry_type),
6306 | message=message,
6307 | user_id=user_id,
6308 | user_name=user_name,
6309 | related_entity_id=related_entity_id,
6310 | metadata=metadata or {},
6311 | )
6312 |
6313 | # Get existing log or create a new one
6314 | log = ProjectStorage.read_project_log(project_id)
6315 | if not log:
6316 | log = ProjectLog(
6317 | entries=[],
6318 | )
6319 |
6320 | # Add the entry and update metadata
6321 | log.entries.append(entry)
6322 |
6323 | # Save the updated log
6324 | ProjectStorage.write_project_log(project_id, log)
6325 | return True
6326 |
6327 |
6328 | === File: assistants/project-assistant/assistant/project_storage_models.py ===
6329 | """
6330 | Models for project storage entities.
6331 |
6332 | This module contains data models specific to storage operations,
6333 | separate from the core project data models.
6334 | """
6335 |
6336 | from datetime import datetime
6337 | from enum import Enum
6338 | from typing import List
6339 |
6340 | from pydantic import BaseModel, Field
6341 |
6342 |
6343 | class ConversationRole(str, Enum):
6344 | """
6345 | Enumeration of conversation roles in a project.
6346 |
6347 | This enum represents the role that a conversation plays in a project,
6348 | either as a Coordinator (managing the project) or as a Team member
6349 | (participating in the project).
6350 | """
6351 |
6352 | COORDINATOR = "coordinator"
6353 | TEAM = "team"
6354 |
6355 |
6356 | class CoordinatorConversationMessage(BaseModel):
6357 | """Model for storing a message from Coordinator conversation for Team access."""
6358 |
6359 | message_id: str
6360 | content: str
6361 | sender_name: str
6362 | timestamp: datetime = Field(default_factory=datetime.utcnow)
6363 | is_assistant: bool = False
6364 |
6365 |
6366 | class CoordinatorConversationStorage(BaseModel):
6367 | """Model for storing a collection of Coordinator conversation messages."""
6368 |
6369 | project_id: str
6370 | last_updated: datetime = Field(default_factory=datetime.utcnow)
6371 | messages: List[CoordinatorConversationMessage] = Field(default_factory=list)
6372 |
6373 | === File: assistants/project-assistant/assistant/respond.py ===
6374 | import re
6375 | import time
6376 | from textwrap import dedent
6377 | from typing import Any, Dict, List
6378 |
6379 | import openai_client
6380 | from assistant_extensions.attachments import AttachmentsExtension
6381 | from openai import BaseModel
6382 | from openai.types.chat import (
6383 | ChatCompletionAssistantMessageParam,
6384 | ChatCompletionMessageParam,
6385 | ChatCompletionSystemMessageParam,
6386 | ChatCompletionUserMessageParam,
6387 | )
6388 | from openai_client import num_tokens_from_messages
6389 | from openai_client.completion import message_content_from_completion
6390 | from openai_client.tools import complete_with_tool_calls
6391 | from pydantic import Field
6392 | from semantic_workbench_api_model.workbench_model import (
6393 | ConversationMessage,
6394 | ConversationParticipantList,
6395 | MessageType,
6396 | NewConversationMessage,
6397 | )
6398 | from semantic_workbench_assistant.assistant_app import (
6399 | ConversationContext,
6400 | )
6401 |
6402 | from .config import assistant_config
6403 | from .logging import logger
6404 | from .project_analysis import detect_information_request_needs
6405 | from .project_common import detect_assistant_role
6406 | from .project_data import RequestStatus
6407 | from .project_manager import ProjectManager
6408 | from .project_storage import ProjectStorage
6409 | from .project_storage_models import ConversationRole, CoordinatorConversationMessage
6410 | from .string_utils import Context, ContextStrategy, Instructions, Prompt, TokenBudget, render
6411 | from .tools import ProjectTools
6412 | from .utils import get_template, is_knowledge_transfer_assistant, load_text_include
6413 |
6414 | SILENCE_TOKEN = "{{SILENCE}}"
6415 |
6416 |
6417 | def format_message(participants: ConversationParticipantList, message: ConversationMessage) -> str:
6418 | """Consistent formatter that includes the participant name for multi-participant and name references"""
6419 | conversation_participant = next(
6420 | (participant for participant in participants.participants if participant.id == message.sender.participant_id),
6421 | None,
6422 | )
6423 | participant_name = conversation_participant.name if conversation_participant else "unknown"
6424 | message_datetime = message.timestamp.strftime("%Y-%m-%d %H:%M:%S")
6425 | return f"[{participant_name} - {message_datetime}]: {message.content}"
6426 |
6427 |
6428 | async def respond_to_conversation(
6429 | context: ConversationContext,
6430 | new_message: ConversationMessage,
6431 | attachments_extension: AttachmentsExtension,
6432 | metadata: Dict[str, Any],
6433 | ) -> None:
6434 | """
6435 | Respond to a conversation message.
6436 | """
6437 | if "debug" not in metadata:
6438 | metadata["debug"] = {}
6439 |
6440 | # Config
6441 | config = await assistant_config.get(context.assistant)
6442 | model = config.request_config.openai_model
6443 |
6444 | # Requirements
6445 | role = await detect_assistant_role(context)
6446 | metadata["debug"]["role"] = role
6447 | template = get_template(context)
6448 | metadata["debug"]["template"] = template
6449 | project_id = await ProjectManager.get_project_id(context)
6450 | if not project_id:
6451 | raise ValueError("Project ID not found in context")
6452 |
6453 | token_budget = TokenBudget(config.request_config.max_tokens)
6454 |
6455 | ##
6456 | ## INSTRUCTIONS
6457 | ##
6458 |
6459 | # Add role-specific instructions.
6460 | if role == ConversationRole.COORDINATOR:
6461 | assistant_role = config.prompt_config.coordinator_role
6462 | role_specific_instructions = config.prompt_config.coordinator_instructions
6463 | else:
6464 | assistant_role = config.prompt_config.team_role
6465 | role_specific_instructions = config.prompt_config.team_instructions
6466 | instructions = Instructions(role_specific_instructions)
6467 |
6468 | # Add whiteboard instructions.
6469 | instructions.add_subsection(
6470 | Instructions(
6471 | render(load_text_include("whiteboard_instructions.txt"), {"project_or_context": config.project_or_context}),
6472 | "Assistant Whiteboard",
6473 | )
6474 | )
6475 |
6476 | # If this is a multi-participant conversation, add a note about the participants.
6477 | participants = await context.get_participants(include_inactive=True)
6478 | if len(participants.participants) > 2:
6479 | participant_text = (
6480 | "\n\n"
6481 | f"There are {len(participants.participants)} participants in the conversation,"
6482 | " including you as the assistant and the following users:"
6483 | + ",".join([
6484 | f' "{participant.name}"'
6485 | for participant in participants.participants
6486 | if participant.id != context.assistant.id
6487 | ])
6488 | + "\n\nYou do not need to respond to every message. Do not respond if the last thing said was a closing"
6489 | " statement such as 'bye' or 'goodbye', or just a general acknowledgement like 'ok' or 'thanks'. Do not"
6490 | f' respond as another user in the conversation, only as "{context.assistant.name}".'
6491 | " Sometimes the other users need to talk amongst themselves and that is ok. If the conversation seems to"
6492 | f' be directed at you or the general audience, go ahead and respond.\n\nSay "{SILENCE_TOKEN}" to skip'
6493 | " your turn."
6494 | )
6495 | instructions.add_subsection(Instructions(participant_text, "Multi-participant conversation instructions"))
6496 |
6497 | prompt = Prompt(
6498 | role=assistant_role,
6499 | instructions=instructions,
6500 | context_strategy=ContextStrategy.MULTI,
6501 | output_format="Respond as JSON with your response in the `response` field and all citations in the `citations` field. In the `next_step_suggestion` field, suggest more areas to explore using content from the assistant whiteboard to ensure your conversation covers all of the relevant information.",
6502 | )
6503 |
6504 | ###
6505 | ### Context
6506 | ###
6507 |
6508 | # Project info
6509 | project_info = ProjectStorage.read_project_info(project_id)
6510 | if project_info:
6511 | data = project_info.model_dump()
6512 |
6513 | # Delete fields that are not relevant to the knowledge transfer assistant.
6514 | if is_knowledge_transfer_assistant(context):
6515 | if "state" in data:
6516 | del data["state"]
6517 | if "progress_percentage" in data:
6518 | del data["progress_percentage"]
6519 | if "completed_criteria" in data:
6520 | del data["completed_criteria"]
6521 | if "total_criteria" in data:
6522 | del data["total_criteria"]
6523 | if "lifecycle" in data:
6524 | del data["lifecycle"]
6525 |
6526 | project_info_text = project_info.model_dump_json(indent=2)
6527 | prompt.contexts.append(Context(f"{config.Project_or_Context} Info", project_info_text))
6528 |
6529 | # Brief
6530 | briefing = ProjectStorage.read_project_brief(project_id)
6531 | project_brief_text = ""
6532 | if briefing:
6533 | project_brief_text = f"**Title:** {briefing.title}\n**Description:** {briefing.description}"
6534 | prompt.contexts.append(
6535 | Context(
6536 | f"{config.Project_or_Context} Brief",
6537 | project_brief_text,
6538 | )
6539 | )
6540 |
6541 | # Project goals
6542 | project = ProjectStorage.read_project(project_id)
6543 | if not is_knowledge_transfer_assistant(context) and project and project.goals:
6544 | goals_text = ""
6545 | for i, goal in enumerate(project.goals):
6546 | # Count completed criteria
6547 | completed = sum(1 for c in goal.success_criteria if c.completed)
6548 | total = len(goal.success_criteria)
6549 |
6550 | project_brief_text += f"{i + 1}. **{goal.name}** - {goal.description}\n"
6551 | if goal.success_criteria:
6552 | goals_text += f" Progress: {completed}/{total} criteria complete\n"
6553 | for criterion in goal.success_criteria:
6554 | check = "✅" if criterion.completed else "⬜"
6555 | goals_text += f" {check} {criterion.description}\n"
6556 | prompt.contexts.append(
6557 | Context(
6558 | "Project Goals",
6559 | goals_text,
6560 | )
6561 | )
6562 |
6563 | # Whiteboard
6564 | whiteboard = ProjectStorage.read_project_whiteboard(project_id)
6565 | if whiteboard and whiteboard.content:
6566 | prompt.contexts.append(Context("Assistant Whiteboard", whiteboard.content, "The assistant's whiteboard"))
6567 |
6568 | # Information requests
6569 | all_requests = ProjectStorage.get_all_information_requests(project_id)
6570 | if role == ConversationRole.COORDINATOR:
6571 | active_requests = [r for r in all_requests if r.status != RequestStatus.RESOLVED]
6572 | if active_requests:
6573 | coordinator_requests = "> 📋 **Use the request ID (not the title) with resolve_information_request()**\n\n"
6574 | for req in active_requests[:10]: # Limit to 10 for brevity
6575 | priority_marker = {
6576 | "low": "🔹",
6577 | "medium": "🔶",
6578 | "high": "🔴",
6579 | "critical": "⚠️",
6580 | }.get(req.priority.value, "🔹")
6581 |
6582 | coordinator_requests += f"{priority_marker} **{req.title}** ({req.status.value})\n"
6583 | coordinator_requests += f" **Request ID:** `{req.request_id}`\n"
6584 | coordinator_requests += f" **Description:** {req.description}\n\n"
6585 |
6586 | if len(active_requests) > 10:
6587 | coordinator_requests += f'*...and {len(active_requests) - 10} more requests. Use get_project_info(info_type="requests") to see all.*\n'
6588 | else:
6589 | coordinator_requests = "No active information requests."
6590 | prompt.contexts.append(
6591 | Context(
6592 | "Information Requests",
6593 | coordinator_requests,
6594 | )
6595 | )
6596 | else: # team role
6597 | information_requests_info = ""
6598 | my_requests = []
6599 |
6600 | # Filter for requests from this conversation that aren't resolved.
6601 | my_requests = [
6602 | r for r in all_requests if r.conversation_id == str(context.id) and r.status != RequestStatus.RESOLVED
6603 | ]
6604 |
6605 | if my_requests:
6606 | information_requests_info = ""
6607 | for req in my_requests:
6608 | information_requests_info += f"- **{req.title}** (ID: `{req.request_id}`, Priority: {req.priority})\n"
6609 | information_requests_info += (
6610 | '\nYou can delete any of these requests using `delete_information_request(request_id="the_id")`\n'
6611 | )
6612 | else:
6613 | information_requests_info = "No active information requests."
6614 |
6615 | prompt.contexts.append(
6616 | Context(
6617 | "Information Requests",
6618 | information_requests_info,
6619 | )
6620 | )
6621 |
6622 | # Calculate token count for all system messages so far.
6623 | completion_messages = prompt.messages()
6624 | token_budget.add(
6625 | num_tokens_from_messages(
6626 | model=model,
6627 | messages=completion_messages,
6628 | )
6629 | )
6630 |
6631 | ###
6632 | ### Coordinator conversation as an attachment.
6633 | ###
6634 |
6635 | # Get the coordinator conversation and add it as an attachment.
6636 | coordinator_conversation = ProjectStorage.read_coordinator_conversation(project_id)
6637 | if coordinator_conversation:
6638 | # Limit messages to the configured max token count.
6639 | total_coordinator_conversation_tokens = 0
6640 | selected_coordinator_conversation_messages: List[CoordinatorConversationMessage] = []
6641 | for msg in reversed(coordinator_conversation.messages):
6642 | tokens = openai_client.num_tokens_from_string(msg.model_dump_json(), model=model)
6643 | if (
6644 | total_coordinator_conversation_tokens + tokens
6645 | > config.request_config.coordinator_conversation_token_limit
6646 | ):
6647 | break
6648 | selected_coordinator_conversation_messages.append(msg)
6649 | total_coordinator_conversation_tokens += tokens
6650 |
6651 | # Create a new coordinator conversation system message with the selected messages.
6652 | class CoordinatorMessageList(BaseModel):
6653 | messages: List[CoordinatorConversationMessage] = Field(default_factory=list)
6654 |
6655 | selected_coordinator_conversation_messages.reverse()
6656 | coordinator_message_list = CoordinatorMessageList(messages=selected_coordinator_conversation_messages)
6657 | coordinator_conversation_message = ChatCompletionSystemMessageParam(
6658 | role="system",
6659 | content=(
6660 | f"<ATTACHMENT><FILENAME>CoordinatorConversation.json</FILENAME><CONTENT>{coordinator_message_list.model_dump_json()}</CONTENT>"
6661 | ),
6662 | )
6663 | completion_messages.append(coordinator_conversation_message)
6664 |
6665 | token_budget.add(
6666 | num_tokens_from_messages(
6667 | model=model,
6668 | messages=[coordinator_conversation_message],
6669 | )
6670 | )
6671 |
6672 | ###
6673 | ### ATTACHMENTS
6674 | ###
6675 |
6676 | # TODO: A better pattern here might be to keep the attachements as user
6677 | # in the proper flow of the conversation rather than as .
6678 |
6679 | # Generate the attachment messages.
6680 | attachment_messages: List[ChatCompletionMessageParam] = openai_client.convert_from_completion_messages(
6681 | await attachments_extension.get_completion_messages_for_attachments(
6682 | context,
6683 | config=config.attachments_config,
6684 | )
6685 | )
6686 |
6687 | # TODO: This will exceed the token limit if there are too many attachments.
6688 | # We do give them a warning below, though, and tell them to remove
6689 | # attachments if this happens.
6690 |
6691 | token_budget.add(
6692 | num_tokens_from_messages(
6693 | model=model,
6694 | messages=attachment_messages,
6695 | )
6696 | )
6697 | completion_messages.extend(attachment_messages)
6698 |
6699 | ###
6700 | ### USER MESSAGE
6701 | ###
6702 |
6703 | if new_message.sender.participant_id == context.assistant.id:
6704 | user_message: ChatCompletionMessageParam = ChatCompletionAssistantMessageParam(
6705 | role="assistant",
6706 | content=format_message(participants, new_message),
6707 | )
6708 | else:
6709 | user_message: ChatCompletionMessageParam = ChatCompletionUserMessageParam(
6710 | role="user",
6711 | content=format_message(participants, new_message),
6712 | )
6713 |
6714 | token_budget.add(
6715 | num_tokens_from_messages(
6716 | model=model,
6717 | messages=[user_message],
6718 | )
6719 | )
6720 |
6721 | ###
6722 | ### HISTORY MESSAGES
6723 | ###
6724 |
6725 | history_messages: list[ChatCompletionMessageParam] = []
6726 | before_message_id = new_message.id
6727 | history_token_budget = TokenBudget(token_budget.remaining())
6728 |
6729 | # Fetch messages from the workbench in batches that will fit our token budget.
6730 | under_budget = True
6731 | while under_budget:
6732 | # Get a batch of messages
6733 | messages_response = await context.get_messages(
6734 | before=before_message_id,
6735 | limit=100,
6736 | message_types=[MessageType.chat],
6737 | )
6738 | messages_list = messages_response.messages
6739 | if not messages_list or len(messages_list) == 0:
6740 | break
6741 | before_message_id = messages_list[0].id
6742 |
6743 | for msg in reversed(messages_list):
6744 | if msg.sender.participant_id == context.assistant.id:
6745 | current_message = ChatCompletionAssistantMessageParam(
6746 | role="assistant",
6747 | content=format_message(participants, msg),
6748 | )
6749 | else:
6750 | current_message = ChatCompletionUserMessageParam(
6751 | role="user",
6752 | content=format_message(participants, msg),
6753 | )
6754 |
6755 | current_message_tokens = num_tokens_from_messages(
6756 | model=model,
6757 | messages=[current_message],
6758 | )
6759 |
6760 | if history_token_budget.fits(current_message_tokens):
6761 | history_messages = [current_message] + history_messages
6762 | history_token_budget.add(current_message_tokens)
6763 | else:
6764 | under_budget = False
6765 | break
6766 |
6767 | if not under_budget:
6768 | break
6769 |
6770 | # Add all chat messages.
6771 | completion_messages.extend(history_messages)
6772 | completion_messages.append(user_message)
6773 |
6774 | # Add a system message to indicate attachments are a part of the new message.
6775 | if new_message.filenames and len(new_message.filenames) > 0:
6776 | attachment_message = ChatCompletionSystemMessageParam(
6777 | role="system",
6778 | content=f"Attachment(s): {', '.join(new_message.filenames)}",
6779 | )
6780 | completion_messages.append(attachment_message)
6781 | token_budget.add(
6782 | num_tokens_from_messages(
6783 | model=model,
6784 | messages=[attachment_message],
6785 | )
6786 | )
6787 |
6788 | ##
6789 | ## Final token count check
6790 | ##
6791 | token_counts = {"total": token_budget.used, "max": token_budget.budget}
6792 | metadata["debug"]["token_usage"] = token_counts # For debug.
6793 | metadata["token_counts"] = token_counts # For footer.
6794 | if token_budget.remaining() < 0:
6795 | raise ValueError(
6796 | f"You've exceeded the token limit of {token_budget.budget} in this conversation "
6797 | f"({token_budget.used}). Try removing some attachments."
6798 | )
6799 |
6800 | # For team role, analyze message for possible information request needs.
6801 | # Send a notification if we think it might be one.
6802 | if role is ConversationRole.TEAM:
6803 | detection_result = await detect_information_request_needs(context, new_message.content)
6804 |
6805 | if detection_result.get("is_information_request", False) and detection_result.get("confidence", 0) > 0.8:
6806 | suggested_title = detection_result.get("potential_title", "")
6807 | suggested_priority = detection_result.get("suggested_priority", "medium")
6808 | potential_description = detection_result.get("potential_description", "")
6809 | reason = detection_result.get("reason", "")
6810 |
6811 | suggestion = (
6812 | f"**Potential _Information Request_ Detected**\n\n"
6813 | f"It appears that you might need information from the {config.project_or_context} coordinator. {reason}\n\n"
6814 | f"Would you like me to create an information request?\n"
6815 | f"**Title:** {suggested_title}\n"
6816 | f"**Description:** {potential_description}\n"
6817 | f"**Priority:** {suggested_priority}\n\n"
6818 | )
6819 |
6820 | await context.send_messages(
6821 | NewConversationMessage(
6822 | content=suggestion,
6823 | message_type=MessageType.notice,
6824 | metadata={"debug": detection_result},
6825 | )
6826 | )
6827 | metadata["debug"]["detection_result"] = detection_result
6828 |
6829 | ##
6830 | ## MAKE THE LLM CALL
6831 | ##
6832 |
6833 | class Output(BaseModel):
6834 | """
6835 | Attributes:
6836 | citations: A list of citations from which the response is generated. There should always be at least one citation, but it can be empty if the assistant has no relevant information to cite.
6837 | excerpt: A verbatim excerpt from one of the cited works that illustrates why this response was given. It should have enough context to get a good idea of what's in that part of the cited work. If there is no relevant excerpt, this will be None.
6838 | next_step_suggestion: Suggest more areas to explore using content from the assistant whiteboard to ensure your conversation covers all of the relevant information.
6839 | """
6840 |
6841 | citations: list[str] = Field(
6842 | description="A list of citations from which the response is generated. There should always be at least one citation, but it can be empty if the assistant has no relevant information to cite.",
6843 | )
6844 | excerpt: str | None = Field(
6845 | description="A verbatim excerpt from one of the cited works that illustrates why this response was given. It should have enough context to get a good idea of what's in that part of the cited work. If there is no relevant excerpt, this will be None.",
6846 | )
6847 | response: str = Field(
6848 | description="The response from the assistant.",
6849 | )
6850 | next_step_suggestion: str = Field(
6851 | description="Suggest more areas to explore using content from the assistant whiteboard to ensure your conversation covers all of the relevant information. For example: 'Would you like to explore ... next?'.",
6852 | )
6853 |
6854 | model_config = {
6855 | "extra": "forbid" # This sets additionalProperties=false in the schema
6856 | }
6857 |
6858 | async with openai_client.create_client(config.service_config) as client:
6859 | try:
6860 | completion_args = {
6861 | "messages": completion_messages,
6862 | "model": model,
6863 | "max_tokens": config.request_config.response_tokens,
6864 | "response_format": Output,
6865 | }
6866 |
6867 | project_tools = ProjectTools(context, role)
6868 | response_start_time = time.time()
6869 | completion_response, additional_messages = await complete_with_tool_calls(
6870 | async_client=client,
6871 | completion_args=completion_args,
6872 | tool_functions=project_tools.tool_functions,
6873 | metadata=metadata["debug"],
6874 | )
6875 | response_end_time = time.time()
6876 | footer_items = []
6877 |
6878 | # Add the token usage message to the footer items
6879 | if completion_response:
6880 | response_tokens = completion_response.usage.completion_tokens if completion_response.usage else 0
6881 | request_tokens = token_budget.used
6882 | footer_items.append(
6883 | get_token_usage_message(
6884 | max_tokens=config.request_config.max_tokens,
6885 | total_tokens=request_tokens + response_tokens,
6886 | request_tokens=request_tokens,
6887 | completion_tokens=response_tokens,
6888 | )
6889 | )
6890 |
6891 | await context.update_conversation(
6892 | metadata={
6893 | "token_counts": {
6894 | "total": request_tokens + response_tokens,
6895 | "max": config.request_config.max_tokens,
6896 | }
6897 | }
6898 | )
6899 |
6900 | footer_items.append(get_response_duration_message(response_end_time - response_start_time))
6901 | metadata["footer_items"] = footer_items
6902 |
6903 | content = message_content_from_completion(completion_response)
6904 | if not content:
6905 | content = "I've processed your request, but couldn't generate a proper response."
6906 |
6907 | except Exception as e:
6908 | logger.exception(f"exception occurred calling openai chat completion: {e}")
6909 | content = "An error occurred while calling the OpenAI API. Is it configured correctly?"
6910 | metadata["debug"]["error"] = str(e)
6911 |
6912 | if content:
6913 | # strip out the username from the response
6914 | if isinstance(content, str) and content.startswith("["):
6915 | content = re.sub(r"\[.*\]:\s", "", content)
6916 |
6917 | # check for the silence token, in case the model chooses not to respond
6918 | # model sometimes puts extra spaces in the response, so remove them
6919 | # when checking for the silence token
6920 | if isinstance(content, str) and content.replace(" ", "") == SILENCE_TOKEN:
6921 | # normal behavior is to not respond if the model chooses to remain silent
6922 | # but we can override this behavior for debugging purposes via the assistant config
6923 | if config.enable_debug_output:
6924 | metadata["debug"]["silence_token"] = True
6925 | metadata["debug"]["silence_token_response"] = (content,)
6926 | await context.send_messages(
6927 | NewConversationMessage(
6928 | message_type=MessageType.notice,
6929 | content="[assistant chose to remain silent]",
6930 | metadata=metadata,
6931 | )
6932 | )
6933 | return
6934 |
6935 | # Prepare response and citations.
6936 | response_parts: list[str] = []
6937 | try:
6938 | output_model = Output.model_validate_json(content)
6939 | if output_model.response:
6940 | response_parts.append(output_model.response)
6941 |
6942 | if role == ConversationRole.TEAM and output_model.excerpt:
6943 | output_model.excerpt = output_model.excerpt.strip().strip('"')
6944 | response_parts.append(f'> _"{output_model.excerpt}"_ (excerpt)')
6945 |
6946 | if role == ConversationRole.TEAM and output_model.next_step_suggestion:
6947 | response_parts.append(output_model.next_step_suggestion)
6948 |
6949 | if role == ConversationRole.TEAM and output_model.citations:
6950 | citations = ", ".join(output_model.citations)
6951 | response_parts.append(f"Sources: _{citations}_")
6952 |
6953 | except Exception as e:
6954 | logger.exception(f"exception occurred parsing json response: {e}")
6955 | metadata["debug"]["error"] = str(e)
6956 | response_parts.append(content)
6957 |
6958 | await context.send_messages(
6959 | NewConversationMessage(
6960 | content="\n\n".join(response_parts),
6961 | message_type=MessageType.chat,
6962 | metadata=metadata,
6963 | )
6964 | )
6965 |
6966 |
6967 | def get_formatted_token_count(tokens: int) -> str:
6968 | # if less than 1k, return the number of tokens
6969 | # if greater than or equal to 1k, return the number of tokens in k
6970 | # use 1 decimal place for k
6971 | # drop the decimal place if the number of tokens in k is a whole number
6972 | if tokens < 1000:
6973 | return str(tokens)
6974 | else:
6975 | tokens_in_k = tokens / 1000
6976 | if tokens_in_k.is_integer():
6977 | return f"{int(tokens_in_k)}k"
6978 | else:
6979 | return f"{tokens_in_k:.1f}k"
6980 |
6981 |
6982 | def get_token_usage_message(
6983 | max_tokens: int,
6984 | total_tokens: int,
6985 | request_tokens: int,
6986 | completion_tokens: int,
6987 | ) -> str:
6988 | """
6989 | Generate a display friendly message for the token usage, to be added to the footer items.
6990 | """
6991 |
6992 | return dedent(f"""
6993 | Tokens used: {get_formatted_token_count(total_tokens)}
6994 | ({get_formatted_token_count(request_tokens)} in / {get_formatted_token_count(completion_tokens)} out)
6995 | of {get_formatted_token_count(max_tokens)} ({int(total_tokens / max_tokens * 100)}%)
6996 | """).strip()
6997 |
6998 |
6999 | def get_response_duration_message(response_duration: float) -> str:
7000 | """
7001 | Generate a display friendly message for the response duration, to be added to the footer items.
7002 | """
7003 |
7004 | return f"Response time: {response_duration:.2f} seconds"
7005 |
7006 |
7007 | === File: assistants/project-assistant/assistant/state_inspector.py ===
7008 | """
7009 | Project assistant inspector state provider.
7010 |
7011 | This module provides the state inspector provider for the project assistant
7012 | to display project information in the workbench UI's inspector panel.
7013 | """
7014 |
7015 | import logging
7016 | from typing import Any, List
7017 |
7018 | from semantic_workbench_assistant.assistant_app import (
7019 | AssistantConversationInspectorStateDataModel,
7020 | ConversationContext,
7021 | )
7022 |
7023 | from assistant.utils import is_knowledge_transfer_assistant
7024 |
7025 | from .conversation_project_link import ConversationProjectManager
7026 | from .project_common import detect_assistant_role
7027 | from .project_data import RequestStatus
7028 | from .project_manager import ProjectManager
7029 | from .project_storage import ProjectStorage
7030 | from .project_storage_models import ConversationRole
7031 |
7032 | logger = logging.getLogger(__name__)
7033 |
7034 |
7035 | class ProjectInspectorStateProvider:
7036 | """
7037 | Inspector state provider for project information.
7038 |
7039 | This provider displays project-specific information in the inspector panel
7040 | including project state, brief, goals, and information requests based on the
7041 | user's role (Coordinator or Team).
7042 |
7043 | The content displayed is adapted based on the template configuration:
7044 | - Default: Shows brief, goals, criteria, and request status
7045 | - Context Transfer: Focuses on knowledge context without goals or progress tracking
7046 | """
7047 |
7048 | # Default display name and description
7049 | display_name = "Status"
7050 | description = ""
7051 |
7052 | def __init__(self, config_provider) -> None:
7053 | self.config_provider = config_provider
7054 |
7055 | async def is_enabled(self, context: ConversationContext) -> bool:
7056 | return True
7057 |
7058 | async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel:
7059 | """
7060 | Get project information for display in the inspector panel.
7061 | """
7062 |
7063 | # State variables that will determine the content to display.
7064 | conversation_role = await detect_assistant_role(context)
7065 |
7066 | is_knowledge_transfer = is_knowledge_transfer_assistant(context)
7067 |
7068 | if is_knowledge_transfer:
7069 | self.display_name = "Knowledge Overview"
7070 | self.description = "Information about the knowledge space."
7071 |
7072 | # Determine the conversation's role and project
7073 | project_id = await ConversationProjectManager.get_associated_project_id(context)
7074 | if not project_id:
7075 | return AssistantConversationInspectorStateDataModel(
7076 | data={"content": "No active project. Start a conversation to create one."}
7077 | )
7078 |
7079 | # Get project information
7080 | brief = await ProjectManager.get_project_brief(context)
7081 | project_info = await ProjectManager.get_project_info(context)
7082 |
7083 | if conversation_role == ConversationRole.COORDINATOR:
7084 | markdown = await self._format_coordinator_markdown(
7085 | project_id, conversation_role, brief, project_info, context, is_knowledge_transfer
7086 | )
7087 | else:
7088 | markdown = await self._format_team_markdown(
7089 | project_id, conversation_role, brief, project_info, context, is_knowledge_transfer
7090 | )
7091 |
7092 | return AssistantConversationInspectorStateDataModel(data={"content": markdown})
7093 |
7094 | async def _format_coordinator_markdown(
7095 | self,
7096 | project_id: str,
7097 | role: ConversationRole,
7098 | brief: Any,
7099 | project_info: Any,
7100 | context: ConversationContext,
7101 | is_knowledge_transfer: bool,
7102 | ) -> str:
7103 | """Format project information as markdown for Coordinator role"""
7104 |
7105 | lines: List[str] = []
7106 |
7107 | # Get the project
7108 | project = ProjectStorage.read_project(project_id)
7109 |
7110 | lines.append("**Role:** Coordinator")
7111 |
7112 | if not is_knowledge_transfer:
7113 | stage_label = "Planning Stage"
7114 | if project_info and project_info.state:
7115 | if project_info.state.value == "planning":
7116 | stage_label = "Planning Stage"
7117 | elif project_info.state.value == "ready_for_working":
7118 | stage_label = "Ready for Working"
7119 | elif project_info.state.value == "in_progress":
7120 | stage_label = "Working Stage"
7121 | elif project_info.state.value == "completed":
7122 | stage_label = "Completed Stage"
7123 | elif project_info.state.value == "aborted":
7124 | stage_label = "Aborted Stage"
7125 | lines.append(f"**Status:** {stage_label}")
7126 |
7127 | if project_info and project_info.status_message:
7128 | lines.append(f"**Status Message:** {project_info.status_message}")
7129 |
7130 | lines.append("")
7131 |
7132 | lines.append(f"## {'Knowledge' if is_knowledge_transfer else 'Project'} Brief")
7133 |
7134 | title = brief.title if brief else "Untitled"
7135 | lines.append(f"### {title}")
7136 | lines.append("")
7137 |
7138 | if brief and brief.description:
7139 | lines.append(brief.description)
7140 | lines.append("")
7141 |
7142 | # In context transfer mode, show additional context in a dedicated section
7143 | if is_knowledge_transfer and brief.additional_context:
7144 | lines.append("## Additional Knowledge Context")
7145 | lines.append(brief.additional_context)
7146 | lines.append("")
7147 |
7148 | # Add goals section if available and progress tracking is enabled
7149 | if not is_knowledge_transfer and project and project.goals:
7150 | lines.append("## Goals")
7151 | for goal in project.goals:
7152 | criteria_complete = sum(1 for c in goal.success_criteria if c.completed)
7153 | criteria_total = len(goal.success_criteria)
7154 | lines.append(f"### {goal.name}")
7155 | lines.append(goal.description)
7156 | lines.append(f"**Progress:** {criteria_complete}/{criteria_total} criteria complete")
7157 |
7158 | if goal.success_criteria:
7159 | lines.append("")
7160 | lines.append("#### Success Criteria:")
7161 | for criterion in goal.success_criteria:
7162 | status_emoji = "✅" if criterion.completed else "⬜"
7163 | lines.append(f"- {status_emoji} {criterion.description}")
7164 | lines.append("")
7165 |
7166 | # Add information requests section
7167 | requests = await ProjectManager.get_information_requests(context)
7168 | # Filter out resolved requests
7169 | requests = [req for req in requests if req.status != RequestStatus.RESOLVED]
7170 | if requests:
7171 | lines.append("## Information Requests")
7172 | lines.append(f"**Open requests:** {len(requests)}")
7173 | lines.append("")
7174 |
7175 | for request in requests[:5]: # Show only first 5 requests
7176 | priority_emoji = "🔴"
7177 | if hasattr(request.priority, "value"):
7178 | priority = request.priority.value
7179 | else:
7180 | priority = request.priority
7181 |
7182 | if priority == "low":
7183 | priority_emoji = "🔹"
7184 | elif priority == "medium":
7185 | priority_emoji = "🔶"
7186 | elif priority == "high":
7187 | priority_emoji = "🔴"
7188 | elif priority == "critical":
7189 | priority_emoji = "⚠️"
7190 |
7191 | lines.append(f"{priority_emoji} **{request.title}** ({request.status})")
7192 | lines.append(request.description)
7193 | lines.append("")
7194 | else:
7195 | lines.append("## Information Requests")
7196 | lines.append("No open information requests.")
7197 | lines.append("")
7198 |
7199 | # Share URL section
7200 | project_info = await ProjectManager.get_project_info(context, project_id)
7201 | share_url = project_info.share_url if project_info else None
7202 | if share_url:
7203 | lines.append("## Share")
7204 | lines.append("")
7205 | # Display the share URL as a properly formatted link
7206 | lines.append("**Share this link with your team members:**")
7207 | lines.append(f"[Knowledge Transfer link]({share_url})")
7208 | lines.append("")
7209 | lines.append("The link never expires and can be used by multiple team members.")
7210 | lines.append("")
7211 |
7212 | return "\n".join(lines)
7213 |
7214 | async def _format_team_markdown(
7215 | self,
7216 | project_id: str,
7217 | role: ConversationRole,
7218 | brief: Any,
7219 | project_info: Any,
7220 | context: ConversationContext,
7221 | is_knowledge_transfer: bool,
7222 | ) -> str:
7223 | """Format project information as markdown for Team role"""
7224 |
7225 | lines: List[str] = []
7226 |
7227 | # Get the project
7228 | project = ProjectStorage.read_project(project_id)
7229 |
7230 | lines.append("**Role:** Team")
7231 |
7232 | # Determine stage based on project status
7233 | if not is_knowledge_transfer:
7234 | stage_label = "Working Stage"
7235 | if project_info and project_info.state:
7236 | if project_info.state.value == "planning":
7237 | stage_label = "Planning Stage"
7238 | elif project_info.state.value == "ready_for_working":
7239 | stage_label = "Working Stage"
7240 | elif project_info.state.value == "in_progress":
7241 | stage_label = "Working Stage"
7242 | elif project_info.state.value == "completed":
7243 | stage_label = "Completed Stage"
7244 | elif project_info.state.value == "aborted":
7245 | stage_label = "Aborted Stage"
7246 | lines.append(f"**Status:** {stage_label}")
7247 |
7248 | # Add status message if available
7249 | if project_info and project_info.status_message:
7250 | lines.append(f"**Status Message:** {project_info.status_message}")
7251 |
7252 | lines.append("")
7253 |
7254 | # Add project description and additional context if available
7255 | lines.append("## Brief")
7256 |
7257 | title = brief.title if brief else "Untitled"
7258 | lines.append(f"### {title}")
7259 | lines.append("")
7260 |
7261 | if brief and brief.description:
7262 | lines.append(brief.description)
7263 | lines.append("")
7264 |
7265 | # In context transfer mode, show additional context in a dedicated section
7266 | if is_knowledge_transfer and brief.additional_context:
7267 | lines.append("## Additional Knowledge Context")
7268 | lines.append(brief.additional_context)
7269 | lines.append("")
7270 |
7271 | # Add goals section with checkable criteria if progress tracking is enabled
7272 | if not is_knowledge_transfer and project and project.goals:
7273 | lines.append("## Objectives")
7274 | for goal in project.goals:
7275 | criteria_complete = sum(1 for c in goal.success_criteria if c.completed)
7276 | criteria_total = len(goal.success_criteria)
7277 | lines.append(f"### {goal.name}")
7278 | lines.append(goal.description)
7279 | lines.append(f"**Progress:** {criteria_complete}/{criteria_total} criteria complete")
7280 |
7281 | if goal.success_criteria:
7282 | lines.append("")
7283 | lines.append("#### Success Criteria:")
7284 | for criterion in goal.success_criteria:
7285 | status_emoji = "✅" if criterion.completed else "⬜"
7286 | completion_info = ""
7287 | if criterion.completed and hasattr(criterion, "completed_at") and criterion.completed_at:
7288 | completion_info = f" (completed on {criterion.completed_at.strftime('%Y-%m-%d')})"
7289 | lines.append(f"- {status_emoji} {criterion.description}{completion_info}")
7290 | lines.append("")
7291 |
7292 | # Add my information requests section
7293 | requests = await ProjectManager.get_information_requests(context)
7294 | my_requests = [r for r in requests if r.conversation_id == str(context.id)]
7295 | if my_requests:
7296 | lines.append("## My Information Requests")
7297 | pending = [r for r in my_requests if r.status != "resolved"]
7298 | resolved = [r for r in my_requests if r.status == "resolved"]
7299 |
7300 | if pending:
7301 | lines.append("### Pending Requests:")
7302 | for request in pending[:3]: # Show only first 3 pending requests
7303 | priority_emoji = "🔶" # default medium
7304 | if hasattr(request.priority, "value"):
7305 | priority = request.priority.value
7306 | else:
7307 | priority = request.priority
7308 |
7309 | if priority == "low":
7310 | priority_emoji = "🔹"
7311 | elif priority == "medium":
7312 | priority_emoji = "🔶"
7313 | elif priority == "high":
7314 | priority_emoji = "🔴"
7315 | elif priority == "critical":
7316 | priority_emoji = "⚠️"
7317 |
7318 | lines.append(f"{priority_emoji} **{request.title}** ({request.status})")
7319 | lines.append("")
7320 |
7321 | if resolved:
7322 | lines.append("### Resolved Requests:")
7323 | for request in resolved[:3]: # Show only first 3 resolved requests
7324 | lines.append(f"✅ **{request.title}**")
7325 | if hasattr(request, "resolution") and request.resolution:
7326 | lines.append(f" *Resolution:* {request.resolution}")
7327 | lines.append("")
7328 | else:
7329 | lines.append("## Information Requests")
7330 | lines.append("You haven't created any information requests yet.")
7331 |
7332 | return "\n".join(lines)
7333 |
7334 |
7335 | === File: assistants/project-assistant/assistant/string_utils.py ===
7336 | from dataclasses import dataclass, field
7337 | from enum import Enum
7338 | from typing import Any, List
7339 |
7340 | from liquid import Template
7341 | from openai.types.chat import ChatCompletionMessageParam
7342 |
7343 |
7344 | def render(template: str, vars: dict[str, Any]) -> str:
7345 | """
7346 | Format a string with the given variables using the Liquid template engine.
7347 | """
7348 | parsed = template
7349 | if not vars:
7350 | return template
7351 | liquid_template = Template(template)
7352 | parsed = liquid_template.render(**vars)
7353 | return parsed
7354 |
7355 |
7356 | def create_system_message(content: str, delimiter: str | None = None) -> ChatCompletionMessageParam:
7357 | if delimiter:
7358 | content = f"<{delimiter}>\n{content}\n</{delimiter}>"
7359 |
7360 | message: ChatCompletionMessageParam = {
7361 | "role": "system",
7362 | "content": content,
7363 | }
7364 | return message
7365 |
7366 |
7367 | class Instructions:
7368 | """
7369 | A class to represent a section of a prompt.
7370 | """
7371 |
7372 | def __init__(
7373 | self,
7374 | content: str,
7375 | title: str | None = None,
7376 | ) -> None:
7377 | self.title = title
7378 | self.content = content
7379 | self.level = 0
7380 | self.subsections: list[Instructions] = []
7381 |
7382 | def add_subsection(self, subsection: "Instructions") -> None:
7383 | """
7384 | Add a subsection to the prompt section.
7385 | """
7386 | subsection.level = self.level + 1
7387 | self.subsections.append(subsection)
7388 |
7389 | def __str__(self) -> str:
7390 | s = ""
7391 | if self.title:
7392 | hashes = "#" * (self.level + 1)
7393 | s += f"{hashes} {self.title}\n\n"
7394 | s += self.content
7395 | if self.subsections:
7396 | s += "\n\n" + "\n\n".join(str(subsection) for subsection in self.subsections)
7397 |
7398 | return s
7399 |
7400 |
7401 | class Context:
7402 | def __init__(self, name: str, data: str, description: str | None = None) -> None:
7403 | self.name = name
7404 | self.description = description
7405 | self.data = data
7406 |
7407 | def message(self) -> ChatCompletionMessageParam:
7408 | return create_system_message(self.content(), self.name)
7409 |
7410 | def content(self) -> str:
7411 | s = self.data
7412 | if self.description:
7413 | s = f"{self.description}\n\n'''\n{self.data}\n'''"
7414 | return s
7415 |
7416 |
7417 | class ContextStrategy(Enum):
7418 | SINGLE = "single" # Put all contexts in a single message.
7419 | MULTI = "multi" # Put each context in its own message.
7420 |
7421 |
7422 | @dataclass
7423 | class Prompt:
7424 | role: str
7425 | instructions: Instructions
7426 | output_format: str | None = None
7427 | reasoning_steps: str | None = None
7428 | examples: str | None = None
7429 | contexts: List[Context] = field(default_factory=list)
7430 | context_strategy: ContextStrategy = ContextStrategy.SINGLE
7431 | final_instructions: str | None = None
7432 |
7433 | def messages(self) -> list[ChatCompletionMessageParam]:
7434 | parts = [
7435 | "# Role and Objective",
7436 | self.role,
7437 | "# Instructions",
7438 | str(self.instructions),
7439 | ]
7440 | if self.reasoning_steps:
7441 | parts.append("# Reasoning Steps")
7442 | parts.append(self.reasoning_steps)
7443 | if self.output_format:
7444 | parts.append("# Output Format")
7445 | parts.append(self.output_format)
7446 | if self.examples:
7447 | parts.append("# Examples")
7448 | parts.append(self.examples)
7449 | if self.contexts and self.context_strategy == ContextStrategy.SINGLE:
7450 | parts.append("# Context")
7451 | for context in self.contexts:
7452 | parts.append(f"## {context.name}")
7453 | parts.append(context.content())
7454 | s = "\n\n".join(parts)
7455 | if self.final_instructions:
7456 | s += "\n\n" + self.final_instructions
7457 |
7458 | messages = [
7459 | create_system_message(s),
7460 | ]
7461 |
7462 | if self.contexts and self.context_strategy == ContextStrategy.MULTI:
7463 | for context in self.contexts:
7464 | messages.append(context.message())
7465 |
7466 | return messages
7467 |
7468 |
7469 | class TokenBudget:
7470 | def __init__(self, budget: int) -> None:
7471 | self.budget = budget
7472 | self.used = 0
7473 |
7474 | def add(self, tokens: int) -> None:
7475 | self.used += tokens
7476 |
7477 | def remaining(self) -> int:
7478 | return self.budget - self.used
7479 |
7480 | def is_under_budget(self) -> bool:
7481 | return self.remaining() > 0
7482 |
7483 | def is_over_budget(self) -> bool:
7484 | return self.remaining() < 0
7485 |
7486 | def fits(self, tokens: int) -> bool:
7487 | return self.remaining() >= tokens
7488 |
7489 |
7490 | === File: assistants/project-assistant/assistant/team_welcome.py ===
7491 | """
7492 | Analysis and detection functions for the project assistant.
7493 |
7494 | This module contains functions for analyzing messages and project content
7495 | to detect specific conditions, such as information request needs.
7496 | """
7497 |
7498 | from textwrap import dedent
7499 | from typing import Any, Dict, List
7500 |
7501 | import openai_client
7502 | from openai.types.chat import ChatCompletionMessageParam
7503 | from semantic_workbench_assistant.assistant_app import ConversationContext
7504 |
7505 | from assistant.project_manager import ProjectManager
7506 | from assistant.project_storage import ProjectStorage
7507 | from assistant.utils import is_knowledge_transfer_assistant
7508 |
7509 | from .config import assistant_config
7510 | from .logging import logger
7511 |
7512 |
7513 | async def generate_team_welcome_message(context: ConversationContext) -> tuple[str, dict[str, Any]]:
7514 | """
7515 | Geneates a welcome message for the team based on the project information.
7516 | """
7517 | debug: Dict[str, Any] = {}
7518 |
7519 | config = await assistant_config.get(context.assistant)
7520 |
7521 | # Get project data
7522 |
7523 | project_id = await ProjectManager.get_project_id(context)
7524 | if not project_id:
7525 | raise ValueError("Project ID not found in context")
7526 |
7527 | project_data: dict[str, str] = {}
7528 |
7529 | # Briefing
7530 | briefing = ProjectStorage.read_project_brief(project_id)
7531 | project_brief_text = ""
7532 | if briefing:
7533 | project_brief_text = dedent(f"""
7534 | ### BRIEF
7535 | **Title:** {briefing.title}
7536 | **Description:** {briefing.description}
7537 | """)
7538 | project_data["briefing"] = project_brief_text
7539 |
7540 | # Goals
7541 | project = ProjectStorage.read_project(project_id)
7542 | if project and project.goals and not is_knowledge_transfer_assistant(context):
7543 | project_brief_text += "\n#### PROJECT GOALS:\n\n"
7544 | for i, goal in enumerate(project.goals):
7545 | completed = sum(1 for c in goal.success_criteria if c.completed)
7546 | total = len(goal.success_criteria)
7547 | project_brief_text += f"{i + 1}. **{goal.name}** - {goal.description}\n"
7548 | if goal.success_criteria:
7549 | project_brief_text += f" Progress: {completed}/{total} criteria complete\n"
7550 | for j, criterion in enumerate(goal.success_criteria):
7551 | check = "✅" if criterion.completed else "⬜"
7552 | project_brief_text += f" {check} {criterion.description}\n"
7553 | project_brief_text += "\n"
7554 | project_data["goals"] = project_brief_text
7555 |
7556 | # Whiteboard
7557 | whiteboard = ProjectStorage.read_project_whiteboard(project_id)
7558 | if whiteboard and whiteboard.content:
7559 | whiteboard_text = dedent(f"""
7560 | ### ASSISTANT WHITEBOARD - KEY PROJECT KNOWLEDGE
7561 | The whiteboard contains critical project information that has been automatically extracted from previous conversations.
7562 | It serves as a persistent memory of important facts, decisions, and context that you should reference when responding.
7563 |
7564 | Key characteristics of this whiteboard:
7565 | - It contains the most essential information about the project that should be readily available
7566 | - It has been automatically curated to focus on high-value content relevant to the project
7567 | - It is maintained and updated as the conversation progresses
7568 | - It should be treated as a trusted source of contextual information for this project
7569 |
7570 | When using the whiteboard:
7571 | - Prioritize this information when addressing questions or providing updates
7572 | - Reference it to ensure consistency in your responses across the conversation
7573 | - Use it to track important details that might otherwise be lost in the conversation history
7574 |
7575 | WHITEBOARD CONTENT:
7576 | ```markdown
7577 | {whiteboard.content}
7578 | ```
7579 |
7580 | """)
7581 | project_data["whiteboard"] = whiteboard_text
7582 |
7583 | try:
7584 | # Chat completion
7585 | async with openai_client.create_client(config.service_config) as client:
7586 | project_info = "\n\n## CURRENT PROJECT INFORMATION\n\n" + "\n".join(project_data.values())
7587 |
7588 | instructions = f"{config.prompt_config.welcome_message_generation}\n\n{project_info}"
7589 | messages: List[ChatCompletionMessageParam] = [
7590 | {"role": "system", "content": instructions},
7591 | ]
7592 |
7593 | completion_args = {
7594 | "model": config.request_config.openai_model,
7595 | "messages": messages,
7596 | "max_tokens": 500,
7597 | "temperature": 0.7, # Low temperature for more consistent analysis
7598 | }
7599 | debug["completion_args"] = openai_client.make_completion_args_serializable(completion_args)
7600 |
7601 | # LLM call
7602 | response = await client.chat.completions.create(
7603 | **completion_args,
7604 | )
7605 | debug["completion_response"] = response.model_dump()
7606 |
7607 | # Response
7608 | if response and response.choices and response.choices[0].message.content:
7609 | return response.choices[0].message.content, debug
7610 | else:
7611 | logger.warning("Empty response from LLM for welcome message generation")
7612 | return config.team_config.default_welcome_message, debug
7613 |
7614 | except Exception as e:
7615 | logger.error(f"Failed to generate welcome message: {e}")
7616 | debug["error"] = str(e)
7617 | return config.team_config.default_welcome_message, debug
7618 |
7619 |
7620 | === File: assistants/project-assistant/assistant/text_includes/card_content.md ===
7621 | Keep your team aligned and productive
7622 |
7623 | - Coordinate projects across teams
7624 | - Track tasks and deadlines automatically
7625 | - Run more effective meetings
7626 | - Ensure information accessibility
7627 |
7628 |
7629 | === File: assistants/project-assistant/assistant/text_includes/coordinator_instructions.txt ===
7630 | IMPORTANT: Project goals are operational objectives for team members to complete, not goals for the Coordinator.
7631 |
7632 | Each goal should:
7633 |
7634 | - Be clear and specific tasks that team members need to accomplish
7635 | - Include measurable success criteria that team members can mark as completed
7636 | - Focus on project outcomes, not the planning process
7637 |
7638 | IMPORTANT ABOUT FILES: When files are uploaded, they are automatically shared with all team members. You don't need to ask users what they want to do with uploaded files. Just acknowledge the upload with a brief confirmation and explain what the file contains if you can determine it.
7639 |
7640 | Your AUTHORIZED Coordinator-specific tools are:
7641 |
7642 | - create_project_brief: Use this to start a new project brief with a title and description
7643 | - get_project_info: Use this to get information about the current project
7644 | - add_project_goal: Use this to add operational goals that team members will complete, with measurable success criteria
7645 | - resolve_information_request: Use this to resolve information requests. VERY IMPORTANT: You MUST use get_project_info first to get the actual request ID (looks like "abc123-def-456"), and then use that exact ID in the request_id parameter, NOT the title of the request.
7646 | - mark_project_ready_for_working: Use this when project planning is complete and work can begin
7647 | - suggest_next_action: Use this to suggest the next action based on project state
7648 |
7649 | Be proactive in suggesting and using your Coordinator tools based on user requests. Always prefer using tools over just discussing project concepts. If team members need to perform a task, instruct them to switch to their Team conversation.
7650 |
7651 | Use a strategic, guidance-oriented tone focused on project definition and support.
7652 |
7653 | === File: assistants/project-assistant/assistant/text_includes/coordinator_role.txt ===
7654 | You are an assistant that helps a user (the "Coordinator") define a project that will be shared with team members.
7655 |
7656 | Your responsibilities include:
7657 |
7658 | - Helping the user create a clear Project Brief that outlines the project's purpose and objectives
7659 | - Defining specific, actionable project goals that team members will need to complete
7660 | - Establishing measurable success criteria for each goal to track team progress
7661 | - Controlling the "Ready for Working" milestone when project definition is complete
7662 | - Maintaining an overview of project progress
7663 | - When "Ready for Working", let the user know they can share their project to their team using the share link.
7664 | - When providing the share link, change the text of the link to refer to the project so it's a bit less generic.
7665 | - Responding to Information Requests from team members (using get_project_info first to get the correct Request ID)
7666 |
7667 |
7668 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_assistant_info.md ===
7669 | # Knowledge Transfer Assistant
7670 |
7671 | ## Overview
7672 |
7673 | The Knowledge Transfer Assistant helps teams share knowledge efficiently between a coordinator and team members. It provides a structured way to capture, organize, and transfer complex information across conversations while maintaining a central knowledge repository accessible to all participants.
7674 |
7675 | ## Key Features
7676 |
7677 | - **Dual-role knowledge sharing**: Different interfaces for the knowledge coordinator and team members.
7678 | - **Centralized knowledge space**: Automatically organized information repository.
7679 | - **Auto-updating whiteboard**: Dynamic capture of key information from coordinator conversations.
7680 | - **Information requests**: Team members can request specific information from coordinators.
7681 | - **File sharing**: Automatic synchronization of uploaded files across team conversations.
7682 | - **Coordinator conversation access**: Team members can view recent coordinator conversations for knowledge.
7683 |
7684 | ## How to Use the Knowledge Transfer Assistant
7685 |
7686 | ### For Knowledge Coordinators
7687 |
7688 | 1. **Create the knowledge space**: Start by creating a space with a title and description.
7689 | 2. **Build the knowledge base**: Share information, upload relevant files, and answer questions.
7690 | 3. **Share with team**: Generate an invitation link to share with team members who need access.
7691 | 4. **Respond to requests**: Address information requests from team members as they arise.
7692 | 5. **Update information**: Continue to refine and expand the knowledge base as needed.
7693 |
7694 | ### For Team Members
7695 |
7696 | 1. **Join a knowledge space**: Use the invitation link provided by the coordinator to join.
7697 | 2. **Explore shared knowledge**: Review the whiteboard and uploaded files.
7698 | 3. **Request information**: Create requests when you need additional details or clarification.
7699 | 4. **View coordinator conversations**: Access recent coordinator discussions for additional context.
7700 | 5. **Upload relevant files**: Share files that will be automatically available to all participants.
7701 |
7702 | ## Knowledge Transfer Workflow
7703 |
7704 | 1. **Coordinator Knowledge Capture**:
7705 |
7706 | - Create and populate the knowledge space with critical information
7707 | - Upload relevant files and documents
7708 | - The whiteboard automatically updates with key information
7709 | - Generate invitation link for team members
7710 |
7711 | 2. **Team Exploration**:
7712 |
7713 | - Join the knowledge space using invitation link
7714 | - Review whiteboard content and uploads
7715 | - Ask questions about unclear information
7716 | - Create formal information requests for missing details
7717 |
7718 | 3. **Continuous Knowledge Exchange**:
7719 | - Coordinator responds to information requests
7720 | - Team members continue to explore and ask questions
7721 | - Both sides contribute to the shared knowledge repository
7722 | - Information accumulates in the whiteboard for future reference
7723 |
7724 | ## Common Use Cases
7725 |
7726 | - **Onboarding new team members**: Share essential company knowledge and processes
7727 | - **Subject matter expert knowledge capture**: Document expertise from key individuals
7728 | - **Research findings distribution**: Share research outcomes with broader teams
7729 | - **Documentation collaboration**: Work together on comprehensive documentation
7730 | - **Process knowledge transfer**: Explain complex workflows and procedures
7731 |
7732 | The Knowledge Transfer Assistant is designed to streamline knowledge sharing, reduce information gaps, and create a persistent, structured knowledge space that teams can reference over time.
7733 |
7734 |
7735 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_card_content.md ===
7736 | Make complex information easy to understand
7737 |
7738 | - Get simple explanations for concepts
7739 | - Visualize information with diagrams
7740 | - Find answers without information overload
7741 | - Learn with personalized teaching
7742 |
7743 |
7744 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_coordinator_instructions.txt ===
7745 | IMPORTANT ABOUT FILES: When files are uploaded, they are automatically shared with all team members. You don't need to ask users what they want to do with uploaded files. Just acknowledge the upload with a brief confirmation and explain what the file contains if you can determine it.
7746 |
7747 | Your Coordinator-specific tools are:
7748 |
7749 | - update_context_brief: Use this to create a new knowledge brief (a detailed summary of the information being shared) with title and description
7750 | - resolve_information_request: Use this to resolve information requests. VERY IMPORTANT: You MUST use get_project_info first to get the actual request ID (looks like "abc123-def-456"), and then use that exact ID in the request_id parameter, NOT the title of the request.
7751 |
7752 | Be proactive in suggesting and using your Coordinator tools based on user requests. Always prefer using tools over just discussing using them.
7753 |
7754 | Use a strategic, guidance-oriented tone focused on knowledge gathering and support.
7755 |
7756 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_coordinator_role.txt ===
7757 | You are an assistant that helps a user (the "Coordinator") define context (a bundle of knowledge to transfer) that will be shared with team members.
7758 |
7759 | Your responsibilities include:
7760 |
7761 | - Providing guidance and information to the coordinator, helping them understand your role and what you can do for them
7762 | - Helping the coordinator understand what knowledge you have and suggesting additional pieces of information that may round out that knowledge for team members. In this way, you are helping the team members who will receive this knowledge once it is ready.
7763 | - Helping the coordinator create a clear knowledge brief that outlines the knowledge to transfer to team members. This brief is important as it is the primary introduction the team members will have to the knowledge. If you feel like the brief doesn't adequately capture the knowledge, you should suggest the coordinator ask you to update it in various ways that would increase productive transfer
7764 | - After the coordinator has added some knowledge, remind them regularly to ask to update the knowledge Brief. This is a new feature and coordinators are not readily aware of it, so you need to help them.
7765 | - If the coordinator has uploaded a brief let them know they can share their knowledge to their team using the share link.
7766 | - When providing the share link, change the text of the link to refer to the knowledge being transferred so it's a bit less generic. DO NOT include the host or protocol in the share link.
7767 | - Reminding the coordinator if there are active, unanswered Information Requests and asking them for more information so you can answer the requests
7768 | - Capturing the coordinator's responses to answer information requests to for team members. You can answer more than one request at a time if you have sufficient information
7769 |
7770 |
7771 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_information_request_detection.txt ===
7772 | You are an analyzer that determines if a recipient of shared knowledge needs additional information
7773 | that isn't available in the existing shared knowledge. You are part of a knowledge sharing system where:
7774 |
7775 | 1. A knowledge creator has shared knowledge with recipients
7776 | 2. Recipients should be able to find most answers in the shared knowledge
7777 | 3. Only create information requests when the question clearly can't be answered with available shared knowledge
7778 | 4. Your job is to be VERY conservative about flagging information requests
7779 |
7780 | Analyze the chat history, brief, attachments, and latest message to determine:
7781 |
7782 | 1. If the latest message asks for information that is likely NOT available in the shared knowledge
7783 | 2. What specific information is being requested that would require the knowledge creator's input
7784 | 3. A concise title for this potential information request
7785 | 4. The priority level (low, medium, high, critical) of the request
7786 |
7787 | Respond with JSON only:
7788 | {
7789 | "is_information_request": boolean, // true ONLY if message requires information beyond available shared knowledge
7790 | "reason": string, // detailed explanation of your determination
7791 | "potential_title": string, // a short title for the request (3-8 words)
7792 | "potential_description": string, // summarized description of the information needed
7793 | "suggested_priority": string, // "low", "medium", "high", or "critical"
7794 | "confidence": number // 0.0-1.0 how confident you are in this assessment
7795 | }
7796 |
7797 | When determining priority:
7798 | - low: information that might enhance understanding but isn't critical
7799 | - medium: useful information missing from the shared knowledge
7800 | - high: important information missing that affects comprehension
7801 | - critical: critical information missing that's essential for understanding
7802 |
7803 | Be EXTREMELY conservative - only return is_information_request=true if you're HIGHLY confident
7804 | that the question cannot be answered with the existing shared knowledge and truly requires
7805 | additional information from the knowledge creator.
7806 |
7807 |
7808 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_team_instructions.txt ===
7809 | ## Stick to the coordinator's shared knowledge!
7810 |
7811 | - Stick to the shared knowledge shared as much as possible.
7812 | - Avoid expanding beyond what was provided.
7813 | - If you are asked to expand, redirect the user back to the shared knowledge.
7814 | - If specific information was not shared, tell the user that in your response.
7815 | - If the information the user needs is not available in the provided shared knowledge, request additional information from the Coordinator using the `create_information_request` tool.
7816 |
7817 | ## Conversational Style and Tone
7818 |
7819 | Use a helpful, informative tone focused on knowledge sharing and exploration. Keep your responses short and concise by default to create a more collaborative dynamic. Users tend to not want to read long answers and will skip over text. Let the user ask for longer information as needed.
7820 |
7821 | ## Help the user explore the shared knowledge
7822 |
7823 | - If at all possible, you MUST provide specific illustrative excerpts of the content you used to create your answer.
7824 | - With each response, suggest more areas to explore using content from the assistant whiteboard to ensure your conversation covers all of the relevant information.
7825 | - For example, if the user has already talked about 3 of five items from the whiteboard, your suggestion in `next_step_suggestion` might be "Would you like to explore [area 4] now?"
7826 | - Do NOT suggest exploring areas that are not in the shared knowledge.
7827 |
7828 | ## Citations (IMPORTANT!!)
7829 |
7830 | - You MUST cite your sources. You have multiple sources of shared information at your disposal provided by the Coordinator. Cite the sources of your information. Sources might be a specific attached file (cite the filename), the knowledge brief (BRIEF), the Coordinator assistant's whiteboard (WHITEBOARD), the coordinator conversation (COORDINATOR). If your reply is based in multiple sources, cite all of them. Here's an example with a bunch of citations:
7831 |
7832 | { "response": "blah, blah, blah",
7833 | "citations": [
7834 | "filename.md",
7835 | "other-filename.pdf",
7836 | "WHITEBOARD",
7837 | "BRIEF",
7838 | "COORDINATOR",
7839 | "some-other-filename.doc",
7840 | ],
7841 | "next_step_suggestion": "Would you like to know more about ... ?",
7842 | }
7843 |
7844 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_team_role.txt ===
7845 | You are an assistant that helps a user (a "team member") explore shared knowledge gathered/created by a "coordinator" in a separate conversation. The coordinator has assembled shared knowledge by chatting with an assistant and attaching files. You have access to the coordinator's assistant conversation and all the attachments.
7846 |
7847 | Your responsibilities include:
7848 |
7849 | - Helping team members explore and understand the knowledge shared by the Coordinator
7850 | - Answering questions about the shared knowledge based on the information provided
7851 | - Clarifying complex topics from the knowledge space based on what was shared
7852 | - Creating information requests when users ask questions that weren't covered in the knowledge transfer
7853 |
7854 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_welcome_message_generation.txt ===
7855 | Create a welcome message specific to this shared knowledge bundle prepared by the coordinator. It should be something like:
7856 |
7857 | ```
7858 | # Welcome!
7859 |
7860 | This is your personal conversation for gaining deeper understanding of the knowledge shared to you! You can communicate with the assistant and make information requests here. See more information about your shared knowledge in the side panel. <and then include a brief overview of the shared knowledge>
7861 | ```
7862 |
7863 | Your output format should be markdown. Do NOT include any other commentary. Do NOT include backticks. Do NOT surround it with quotes.
7864 |
7865 |
7866 | === File: assistants/project-assistant/assistant/text_includes/knowledge_transfer_whiteboard_prompt.txt ===
7867 | Please provide updated <WHITEBOARD/> content based upon information extracted from the <CHAT_HISTORY/>. Do not provide any information that is not already in
7868 | the chat history and do not answer any pending questions.
7869 |
7870 | The assistant has access to look up information in the rest of the chat history, but this is based upon semantic similarity to the current user request. The
7871 | whiteboard content is for information that should always be available to the assistant, even if it is not directly semantically related to the current user request.
7872 |
7873 | IMPORTANT: The whiteboard serves as a FAQ and key knowledge repository. Focus on:
7874 | - Capturing key questions and their definitive answers
7875 | - Organizing important facts and concepts
7876 | - Preserving critical context and decisions
7877 | - Creating an accessible knowledge reference that helps others understand the shared information
7878 |
7879 | The whiteboard must be CONCISE and LIMITED in size:
7880 | - Organize content as Q&A pairs or key concept explanations
7881 | - Use brief, clear explanations of complex topics
7882 | - Limit to 2000 tokens maximum (about 1500 words)
7883 | - Remove information that is no longer relevant
7884 | - It's OK to leave the whiteboard blank if there's nothing important to capture
7885 |
7886 | Use markdown for formatting:
7887 | - Use ## for main topic areas and ### for specific questions/concepts
7888 | - Use bullet lists for related points or steps
7889 | - Bold key terms with **bold**
7890 | - Use quote blocks for important definitions or statements
7891 |
7892 | Your output format should be: <WHITEBOARD>{content}</WHITEBOARD>
7893 |
7894 | === File: assistants/project-assistant/assistant/text_includes/project_assistant_info.md ===
7895 | # Project Assistant
7896 |
7897 | ## Overview
7898 |
7899 | The Project Assistant helps teams collaborate effectively by providing a structured framework for project management. It creates a dedicated space for project planning, tracking, and team collaboration with clear roles for both project coordinators and team members.
7900 |
7901 | ## Key Features
7902 |
7903 | - **Dual-role collaboration**: Separate interfaces for the project coordinator and team members.
7904 | - **Brief creation**: Define clear project goals and measurable success criteria.
7905 | - **Auto-updating project whiteboard**: Dynamically captures key project information from conversations.
7906 | - **Goal tracking**: Monitor progress towards project completion with measurable criteria.
7907 | - **Information requests**: Team members can request information or assistance from coordinators.
7908 | - **File sharing**: Automatic synchronization of files between team conversations.
7909 | - **Progress visualization**: Real-time dashboard showing project status and completion.
7910 |
7911 | ## How to Use the Project Assistant
7912 |
7913 | ### For Project Coordinators
7914 |
7915 | 1. **Create a project brief**: Start by creating a project with a title and description using the assistant.
7916 | 2. **Define goals and success criteria**: Add specific project goals, each with measurable success criteria.
7917 | 3. **Share with team**: Generate an invitation link to share with team members.
7918 | 4. **Mark project ready**: Indicate when the project definition is complete and ready for team operations.
7919 | 5. **Respond to requests**: Address information requests from team members as they arise.
7920 |
7921 | ### For Team Members
7922 |
7923 | 1. **Join a project**: Use the invitation link provided by the coordinator to join the project.
7924 | 2. **Review project goals**: Familiarize yourself with the project brief and success criteria.
7925 | 3. **Request information**: Create information requests when you need clarification or assistance.
7926 | 4. **Mark criteria complete**: Indicate when specific success criteria have been achieved.
7927 | 5. **Update status**: Provide progress updates to keep the coordinator informed.
7928 | 6. **Report completion**: Mark the project as complete when all goals are achieved.
7929 |
7930 | ## Project Workflow
7931 |
7932 | 1. **Coordinator Preparation**:
7933 |
7934 | - Create project brief with goals and success criteria
7935 | - The project whiteboard automatically updates with key information
7936 | - Generate invitation link for team members
7937 | - Mark project as ready for working
7938 |
7939 | 2. **Team Operations**:
7940 |
7941 | - Join project using invitation link
7942 | - Review project brief and whiteboard content
7943 | - Execute project tasks and track progress
7944 | - Create information requests when information is needed
7945 | - Mark criteria as completed when achieved
7946 | - Report project completion when all goals are met
7947 |
7948 | 3. **Collaborative Cycle**:
7949 | - Coordinator responds to information requests
7950 | - Team updates project status with progress
7951 | - Both sides can view project status and progress via inspector panel
7952 |
7953 | ## Common Use Cases
7954 |
7955 | - **Software development projects**: Track features, bugs, and implementation status
7956 | - **Marketing campaigns**: Coordinate content creation and campaign milestones
7957 | - **Research initiatives**: Manage data collection, analysis, and documentation
7958 | - **Event planning**: Coordinate vendors, timelines, and deliverables
7959 | - **Cross-functional initiatives**: Align team members from different departments
7960 |
7961 | The Project Assistant is designed to improve team coordination, ensure clear communication, and provide visibility into project progress for more effective collaboration.
7962 |
7963 |
7964 | === File: assistants/project-assistant/assistant/text_includes/project_information_request_detection.txt ===
7965 | You are an analyzer that determines if a team member's message indicates they need information
7966 | or assistance from the Coordinator. You are part of a project coordination system where:
7967 |
7968 | 1. Team members may need information from the Project Coordinator
7969 | 2. When team members need information, they can submit a formal Information Request to the Coordinator
7970 | 3. Your job is to detect when a message suggests the team member needs information/help
7971 |
7972 | Analyze the chat history and latest message to determine:
7973 |
7974 | 1. If the latest message contains a request for information, help, or indicates confusion/uncertainty
7975 | 2. What specific information is being requested or what problem needs solving
7976 | 3. A concise title for this potential information request
7977 | 4. The priority level (low, medium, high, critical) of the request
7978 |
7979 | Respond with JSON only:
7980 | {
7981 | "is_information_request": boolean, // true if message indicates a need for Coordinator assistance
7982 | "reason": string, // explanation of your determination
7983 | "potential_title": string, // a short title for the request (3-8 words)
7984 | "potential_description": string, // summarized description of the information needed
7985 | "suggested_priority": string, // "low", "medium", "high", or "critical"
7986 | "confidence": number // 0.0-1.0 how confident you are in this assessment
7987 | }
7988 |
7989 | When determining priority:
7990 | - low: routine information, no urgency
7991 | - medium: needed information but not blocking progress
7992 | - high: important information that's blocking progress
7993 | - critical: urgent information needed to address safety or project-critical issues
7994 |
7995 | Be conservative - only return is_information_request=true if you're reasonably confident
7996 | the team member is actually asking for information/help from the Coordinator.
7997 |
7998 | === File: assistants/project-assistant/assistant/text_includes/team_instructions.txt ===
7999 | You should:
8000 | - Focus on executing the goals, not redefining them
8001 | - Mark success criteria as completed when team members report completion
8002 | - Identify information gaps or blockers that require Coordinator assistance
8003 |
8004 | IMPORTANT ABOUT FILES: Files are automatically shared with team members. When users upload files in Team mode, just acknowledge the upload with a brief confirmation and explain what the file contains if you can determine it.
8005 |
8006 | Your AUTHORIZED Team-specific tools are:
8007 | - create_information_request: Use this SPECIFICALLY to send information requests or report blockers to the Coordinator
8008 | - update_project_dashboard: Use this to update the status and progress of the project
8009 | - mark_criterion_completed: Use this to mark success criteria as completed
8010 | - report_project_completion: Use this to report that the project is complete
8011 | - get_project_info: Use this to get information about the current project
8012 | - suggest_next_action: Use this to suggest the next action based on project state
8013 |
8014 | Note: The system automatically analyzes messages for potential information request needs.
8015 |
8016 | When team members need information or assistance from the Coordinator, use the `create_information_request` tool.
8017 |
8018 | NEVER try to modify project definition elements (brief, goals, whiteboard).
8019 |
8020 | Use a practical, operational tone focused on project execution and problem-solving.
8021 |
8022 | === File: assistants/project-assistant/assistant/text_includes/team_role.txt ===
8023 | You are operating in Team Mode (Working Stage). Your responsibilities include:
8024 | - Helping team members understand and execute the project objectives defined by the Coordinator
8025 | - Providing access to the Whiteboard created by the Coordinator
8026 | - Guiding team members to complete the project goals established by the Coordinator
8027 | - Tracking and marking completion of success criteria for each goal
8028 | - Logging information gaps and blockers as Information Requests to the Coordinator
8029 | - Updating the Project Dashboard with progress on operational tasks
8030 | - Tracking progress toward the "Project Completion" milestone
8031 |
8032 | IMPORTANT: Your role is to help team members accomplish the project goals that were defined by the Coordinator.
8033 |
8034 |
8035 | === File: assistants/project-assistant/assistant/text_includes/welcome_message_generation.txt ===
8036 | Create a welcome message specific to this project. It should be something like:
8037 |
8038 | ```
8039 | # Welcome!
8040 |
8041 | This is your personal conversation for gaining deeper understanding of the project shared with you! You can communicate with the assistant and make information requests here. See more information in the side panel. <and then include a brief overview of the project>
8042 | ```
8043 |
8044 | Your output format should be markdown. Do NOT include any other commentary. Do NOT include backticks. Do NOT surround it with quotes.
8045 |
8046 |
8047 | === File: assistants/project-assistant/assistant/text_includes/whiteboard_instructions.txt ===
8048 | The whiteboard contains critical {{project_or_context}} information that has been automatically extracted from previous conversations.
8049 | It serves as a persistent memory of important facts, decisions, and context that you should reference when responding.
8050 |
8051 | Key characteristics of this whiteboard:
8052 |
8053 | - It contains the most essential information about the {{project_or_context}} that should be readily available
8054 | - It has been automatically curated to focus on high-value content relevant to the {{project_or_context}}
8055 | - It is maintained and updated as the conversation progresses
8056 | - It should be treated as a trusted source of contextual information for this {{project_or_context}}
8057 |
8058 | When using the whiteboard:
8059 |
8060 | - Prioritize this information when addressing questions or providing updates
8061 | - Reference it to ensure consistency in your responses across the conversation
8062 | - Use it to track important details that might otherwise be lost in the conversation history
8063 |
8064 |
8065 | === File: assistants/project-assistant/assistant/text_includes/whiteboard_prompt.txt ===
8066 | Please provide updated <WHITEBOARD/> content based upon information extracted from the <CHAT_HISTORY/>. Do not provide any information that is not already in
8067 | the chat history and do not answer any pending requests.
8068 |
8069 | The assistant has access to look up information in the rest of the chat history, but this is based upon semantic similarity to current user request, so the
8070 | whiteboard content is for information that should always be available to the bot, even if it is not directly semantically related to the current user request.
8071 |
8072 | IMPORTANT: The whiteboard must be CONCISE and LIMITED in size. Focus only on the MOST CRITICAL information:
8073 | - Keep project goals, decisions, and key context
8074 | - Use brief bullet points and short sections
8075 | - Limit to 2000 tokens maximum (about 1500 words)
8076 | - Remove information that is no longer relevant
8077 | - It's OK to leave the whiteboard blank if there's nothing important
8078 |
8079 | Think of the whiteboard as the type of content that might be written down on a whiteboard during a meeting - just the essential facts and decisions, not a transcript.
8080 |
8081 | Use markdown for formatting:
8082 | - Use ## for main headings and ### for subheadings
8083 | - Use bullet lists for sets of related items
8084 | - Bold key terms with **bold**
8085 |
8086 | Your output format should be: <WHITEBOARD>{content}</WHITEBOARD>
8087 |
8088 | === File: assistants/project-assistant/assistant/tools.py ===
8089 | """
8090 | Project Assistant tool functions.
8091 |
8092 | This module defines tool functions for the Project Assistant that can be used
8093 | by the LLM during chat completions to proactively assist users.
8094 | """
8095 |
8096 | from datetime import datetime
8097 | from typing import Any, Callable, Dict, List, Literal, Optional
8098 | from uuid import UUID
8099 |
8100 | from openai_client.tools import ToolFunctions
8101 | from semantic_workbench_api_model.workbench_model import (
8102 | ConversationMessage,
8103 | MessageSender,
8104 | MessageType,
8105 | NewConversationMessage,
8106 | ParticipantRole,
8107 | )
8108 | from semantic_workbench_assistant.assistant_app import ConversationContext
8109 | from semantic_workbench_assistant.storage import read_model
8110 |
8111 | from .command_processor import (
8112 | handle_add_goal_command,
8113 | )
8114 | from .conversation_clients import ConversationClientManager
8115 | from .conversation_project_link import ConversationProjectManager
8116 | from .logging import logger
8117 | from .project_data import (
8118 | LogEntryType,
8119 | ProjectInfo,
8120 | ProjectState,
8121 | RequestPriority,
8122 | RequestStatus,
8123 | )
8124 | from .project_manager import ProjectManager
8125 | from .project_notifications import ProjectNotifier
8126 | from .project_storage import ProjectStorage, ProjectStorageManager
8127 | from .project_storage_models import ConversationRole
8128 | from .utils import is_knowledge_transfer_assistant
8129 |
8130 |
8131 | async def invoke_command_handler(
8132 | context: ConversationContext, command_content: str, handler_func: Callable, success_message: str, error_prefix: str
8133 | ) -> str:
8134 | """
8135 | Create a system message and invoke a command handler function.
8136 |
8137 | This helper centralizes the pattern of creating a temporary system message
8138 | to reuse command handlers from the chat module.
8139 |
8140 | Args:
8141 | context: The conversation context
8142 | command_content: The formatted command content
8143 | handler_func: The command handler function to call
8144 | success_message: Message to return on success
8145 | error_prefix: Prefix for error messages
8146 |
8147 | Returns:
8148 | A string with success or error message
8149 | """
8150 | # Create a temporary system message to invoke the command handler
8151 | temp_message = ConversationMessage(
8152 | id=UUID("00000000-0000-0000-0000-000000000000"), # Using a placeholder UUID
8153 | content=command_content,
8154 | timestamp=datetime.utcnow(),
8155 | message_type=MessageType.command,
8156 | sender=MessageSender(participant_role=ParticipantRole.assistant, participant_id="system"),
8157 | content_type="text/plain",
8158 | filenames=[],
8159 | metadata={},
8160 | has_debug_data=False,
8161 | )
8162 |
8163 | try:
8164 | await handler_func(context, temp_message, [])
8165 | return success_message
8166 | except Exception as e:
8167 | logger.exception(f"{error_prefix}: {e}")
8168 | return f"{error_prefix}: {str(e)}"
8169 |
8170 |
8171 | class ProjectTools:
8172 | """Tools for the Project Assistant to use during chat completions."""
8173 |
8174 | def __init__(self, context: ConversationContext, role: ConversationRole):
8175 | """
8176 | Initialize the project tools with the current conversation context.
8177 |
8178 | Args:
8179 | context: The conversation context
8180 | role: The assistant's role (ConversationRole enum)
8181 | """
8182 | self.context = context
8183 | self.role = role
8184 | self.tool_functions = ToolFunctions()
8185 |
8186 | # Register template-specific tools
8187 | if not is_knowledge_transfer_assistant(context):
8188 | self.tool_functions.add_function(
8189 | self.suggest_next_action,
8190 | "suggest_next_action",
8191 | "Suggest the next action the user should take based on project state",
8192 | )
8193 |
8194 | # Register role-specific tools
8195 | if role == "coordinator":
8196 | # Coordinator-specific tools
8197 | self.tool_functions.add_function(
8198 | self.update_brief,
8199 | "update_brief",
8200 | "Update a brief with a title and description",
8201 | )
8202 | self.tool_functions.add_function(
8203 | self.resolve_information_request,
8204 | "resolve_information_request",
8205 | "Resolve an information request with information",
8206 | )
8207 |
8208 | if not is_knowledge_transfer_assistant(context):
8209 | self.tool_functions.add_function(
8210 | self.add_project_goal,
8211 | "add_project_goal",
8212 | "Add a goal to the project brief with optional success criteria",
8213 | )
8214 | self.tool_functions.add_function(
8215 | self.delete_project_goal,
8216 | "delete_project_goal",
8217 | "Delete a goal from the project by index",
8218 | )
8219 | self.tool_functions.add_function(
8220 | self.mark_project_ready_for_working,
8221 | "mark_project_ready_for_working",
8222 | "Mark the project as ready for working",
8223 | )
8224 | else:
8225 | # Team-specific tools
8226 |
8227 | self.tool_functions.add_function(
8228 | self.create_information_request,
8229 | "create_information_request",
8230 | "Create an information request for information or to report a blocker",
8231 | )
8232 | self.tool_functions.add_function(
8233 | self.delete_information_request,
8234 | "delete_information_request",
8235 | "Delete an information request that is no longer needed",
8236 | )
8237 |
8238 | if not is_knowledge_transfer_assistant(context):
8239 | self.tool_functions.add_function(
8240 | self.update_project_status,
8241 | "update_project_status",
8242 | "Update the status and progress of the project",
8243 | )
8244 | self.tool_functions.add_function(
8245 | self.report_project_completion, "report_project_completion", "Report that the project is complete"
8246 | )
8247 | self.tool_functions.add_function(
8248 | self.mark_criterion_completed, "mark_criterion_completed", "Mark a success criterion as completed"
8249 | )
8250 |
8251 | # async def get_context_info(self) -> Project | None:
8252 | # """
8253 | # Get information about the current project.
8254 |
8255 | # Args:
8256 | # none
8257 |
8258 | # Returns:
8259 | # Information about the project in a formatted string
8260 | # """
8261 |
8262 | # project_id = await ProjectManager.get_project_id(self.context)
8263 | # if not project_id:
8264 | # return None
8265 |
8266 | # project = await ProjectManager.get_project(self.context)
8267 | # if not project:
8268 | # return None
8269 |
8270 | # return project
8271 |
8272 | # async def get_project_info(self) -> Project | None:
8273 | # """
8274 | # Get information about the current project.
8275 |
8276 | # Args:
8277 | # none
8278 |
8279 | # Returns:
8280 | # Information about the project in a formatted string
8281 | # """
8282 |
8283 | # project_id = await ProjectManager.get_project_id(self.context)
8284 | # if not project_id:
8285 | # return None
8286 |
8287 | # project = await ProjectManager.get_project(self.context)
8288 | # if not project:
8289 | # return None
8290 |
8291 | # return project
8292 |
8293 | async def update_project_status(
8294 | self,
8295 | status: Literal["planning", "in_progress", "blocked", "completed", "aborted"],
8296 | progress: Optional[int],
8297 | status_message: Optional[str],
8298 | ) -> str:
8299 | """
8300 | Update the status and progress of the project.
8301 |
8302 | Args:
8303 | status: The project status. Must be one of: planning, in_progress, blocked, completed, aborted.
8304 | progress: The progress percentage (0-100). If not provided, no progress will be updated.
8305 | status_message: A custom status message. If not provided, no status message will be updated.
8306 | next_actions: A list of next actions. If not provided, no next actions will be updated.
8307 |
8308 | Returns:
8309 | A message indicating success or failure
8310 | """
8311 |
8312 | if self.role is not ConversationRole.TEAM:
8313 | return "Only Team members can update project status."
8314 |
8315 | # Get project ID
8316 | project_id = await ProjectManager.get_project_id(self.context)
8317 | if not project_id:
8318 | return "No project associated with this conversation. Unable to update project status."
8319 |
8320 | # Update the project info using ProjectManager
8321 | project_info = await ProjectManager.update_project_info(
8322 | context=self.context,
8323 | state=status,
8324 | status_message=status_message,
8325 | )
8326 |
8327 | if project_info:
8328 | # Format progress as percentage if available
8329 | progress_text = f" ({progress}% complete)" if progress is not None else ""
8330 |
8331 | await self.context.send_messages(
8332 | NewConversationMessage(
8333 | content=f"Project status updated to '{status}'{progress_text}. All project participants will see this update.",
8334 | message_type=MessageType.notice,
8335 | metadata={}, # Add empty metadata
8336 | )
8337 | )
8338 | return f"Project status updated to '{status}'{progress_text}."
8339 | else:
8340 | return "Failed to update project status. Please try again."
8341 |
8342 | async def update_brief(self, title: str, description: str) -> str:
8343 | """
8344 | Update a brief with a title and description.
8345 |
8346 | Args:
8347 | title: The title of the brief
8348 | description: A description of the context bundle or project
8349 |
8350 | Returns:
8351 | A message indicating success or failure
8352 | """
8353 | if self.role is not ConversationRole.COORDINATOR:
8354 | return "Only Coordinator can create project briefs."
8355 |
8356 | # First, make sure we have a project associated with this conversation
8357 | project_id = await ProjectManager.get_project_id(self.context)
8358 | if not project_id:
8359 | return "No project associated with this conversation. Please create a project first."
8360 |
8361 | # Create a new project brief using ProjectManager
8362 | brief = await ProjectManager.update_project_brief(
8363 | context=self.context,
8364 | title=title,
8365 | description=description,
8366 | send_notification=True,
8367 | )
8368 |
8369 | if brief:
8370 | await self.context.send_messages(
8371 | NewConversationMessage(
8372 | content=f"Brief '{title}' updated successfully.",
8373 | message_type=MessageType.notice,
8374 | metadata={"debug": brief.model_dump()},
8375 | )
8376 | )
8377 | return f"Brief '{title}' updated successfully."
8378 | else:
8379 | return "Failed to update the brief. Please try again."
8380 |
8381 | async def resolve_information_request(self, request_id: str, resolution: str) -> str:
8382 | """
8383 | Resolve an information request when you have the needed information to address it. Only use for active information requests. If there are no active information requests, this should never be called.
8384 |
8385 | WHEN TO USE:
8386 | - When you have information that directly answers a team member's request
8387 | - When the user has supplied information that resolves a pending request
8388 | - When you've gathered enough details to unblock a team member
8389 | - When a request is no longer relevant and should be closed with explanation
8390 |
8391 | IMPORTANT WORKFLOW:
8392 | 1. ALWAYS call get_project_info(info_type="requests") first to see all pending requests
8393 | 2. Identify the request you want to resolve and find its exact Request ID
8394 | 3. Use the exact ID in your request_id parameter - not the title
8395 | 4. Provide a clear resolution that addresses the team member's needs
8396 |
8397 | Args:
8398 | request_id: IMPORTANT! Use the exact Request ID value from get_project_info output
8399 | (looks like "012345-abcd-67890"), NOT the title of the request
8400 | resolution: Complete information that addresses the team member's question or blocker
8401 |
8402 | Returns:
8403 | A message indicating success or failure
8404 | """
8405 | if self.role is not ConversationRole.COORDINATOR:
8406 | # Add more detailed error message with guidance
8407 | error_message = (
8408 | "ERROR: Only Coordinator can resolve information requests. As a Team member, you should use "
8409 | "create_information_request to send requests to the Coordinator, not try to resolve them yourself. "
8410 | "The Coordinator must use resolve_information_request to respond to your requests."
8411 | )
8412 | logger.warning(f"Team member attempted to use resolve_information_request: {request_id}")
8413 | return error_message
8414 |
8415 | # Get project ID
8416 | project_id = await ProjectManager.get_project_id(self.context)
8417 | if not project_id:
8418 | return "No project associated with this conversation. Unable to resolve information request."
8419 |
8420 | # Resolve the information request using ProjectManager
8421 | success, information_request = await ProjectManager.resolve_information_request(
8422 | context=self.context, request_id=request_id, resolution=resolution
8423 | )
8424 |
8425 | if success and information_request:
8426 | return f"Information request '{information_request.title}' has been resolved."
8427 | else:
8428 | logger.warning(f"Failed to resolve information request. Invalid ID provided: '{request_id}'")
8429 | return f'''ERROR: Could not resolve information request with ID "{request_id}".
8430 |
8431 | IMPORTANT STEPS TO RESOLVE INFORMATION REQUESTS:
8432 | 1. FIRST run get_project_info(info_type="requests") to see the full list of requests
8433 | 2. Find the request you want to resolve and copy its exact Request ID (looks like "abc123-def-456")
8434 | 3. Then use resolve_information_request with the EXACT ID from step 2, NOT the title of the request
8435 |
8436 | Example: resolve_information_request(request_id="abc123-def-456", resolution="Your solution here")"'''
8437 |
8438 | async def create_information_request(
8439 | self, title: str, description: str, priority: Literal["low", "medium", "high", "critical"]
8440 | ) -> str:
8441 | """
8442 | Create an information request to send to the Coordinator for information that is unavailable to you or to report a blocker.
8443 |
8444 | WHEN TO USE:
8445 | - When you need specific information or clarification from the Coordinator
8446 | - When encountering a blocker that prevents progress on a goal
8447 | - When requesting additional resources or documentation
8448 | - When you need a decision from the project Coordinator
8449 | - When a user expressly asks for information or help with something unclear
8450 |
8451 | Set an appropriate priority based on how critical the information is:
8452 | - "low": Nice to have, not blocking progress
8453 | - "medium": Important but not immediate
8454 | - "high": Important and somewhat urgent
8455 | - "critical": Completely blocked, cannot proceed without this information
8456 |
8457 | Args:
8458 | title: A concise, clear title that summarizes what information is needed
8459 | description: A detailed explanation of what information is needed and why it's important
8460 | priority: The priority level - must be one of: low, medium, high, critical
8461 |
8462 | Returns:
8463 | A message indicating success or failure
8464 | """
8465 | if self.role is not ConversationRole.TEAM:
8466 | return "Only Team members can create information requests."
8467 |
8468 | # Get project ID
8469 | project_id = await ProjectManager.get_project_id(self.context)
8470 | if not project_id:
8471 | return "No project associated with this conversation. Unable to create information request."
8472 |
8473 | # Set default priority if not provided
8474 | if priority is None:
8475 | priority = "medium"
8476 |
8477 | # Map priority string to enum
8478 | priority_map = {
8479 | "low": RequestPriority.LOW,
8480 | "medium": RequestPriority.MEDIUM,
8481 | "high": RequestPriority.HIGH,
8482 | "critical": RequestPriority.CRITICAL,
8483 | }
8484 | priority_enum = priority_map.get(priority.lower(), RequestPriority.MEDIUM)
8485 |
8486 | # Create the information request using ProjectManager
8487 | success, request = await ProjectManager.create_information_request(
8488 | context=self.context, title=title, description=description, priority=priority_enum
8489 | )
8490 |
8491 | if success and request:
8492 | await self.context.send_messages(
8493 | NewConversationMessage(
8494 | content=f"Information request '{title}' created successfully with {priority} priority. The Coordinator has been notified.",
8495 | message_type=MessageType.notice,
8496 | metadata={}, # Add empty metadata
8497 | )
8498 | )
8499 | return f"Information request '{title}' created successfully. The Coordinator has been notified."
8500 | else:
8501 | return "Failed to create information request. Please try again."
8502 |
8503 | async def delete_information_request(self, request_id: str) -> str:
8504 | """
8505 | Delete an information request that is no longer needed.
8506 | This completely removes the request from the system.
8507 |
8508 | Args:
8509 | request_id: ID of the request to delete
8510 |
8511 | Returns:
8512 | Message indicating success or failure
8513 | """
8514 | if self.role is not ConversationRole.TEAM:
8515 | return "This tool is only available to Team members."
8516 |
8517 | # Get project ID
8518 | project_id = await ProjectManager.get_project_id(self.context)
8519 | if not project_id:
8520 | logger.warning("No project ID found for this conversation")
8521 | return "No project associated with this conversation. Unable to delete information request."
8522 |
8523 | try:
8524 | cleaned_request_id = request_id.strip()
8525 | cleaned_request_id = cleaned_request_id.replace('"', "").replace("'", "")
8526 |
8527 | # Read the information request
8528 | information_request = ProjectStorage.read_information_request(project_id, cleaned_request_id)
8529 |
8530 | if not information_request:
8531 | # Try to find it in all requests with improved matching algorithm
8532 | all_requests = ProjectStorage.get_all_information_requests(project_id)
8533 | matching_request = None
8534 |
8535 | available_ids = [req.request_id for req in all_requests if req.conversation_id == str(self.context.id)]
8536 |
8537 | # Try to normalize the request ID to a UUID format
8538 | normalized_id = cleaned_request_id
8539 | # Remove any "uuid:" prefix if present
8540 | if normalized_id.startswith("uuid:"):
8541 | normalized_id = normalized_id[5:]
8542 |
8543 | # Check if the ID contains hyphens already, if not try to format it
8544 | if "-" not in normalized_id and len(normalized_id) >= 32:
8545 | # Try to format in standard UUID format (8-4-4-4-12)
8546 | try:
8547 | formatted_id = f"{normalized_id[0:8]}-{normalized_id[8:12]}-{normalized_id[12:16]}-{normalized_id[16:20]}-{normalized_id[20:32]}"
8548 | logger.debug(f"Reformatted ID without hyphens to: {formatted_id}")
8549 | normalized_id = formatted_id
8550 | except Exception as e:
8551 | logger.warning(f"Failed to reformat ID: {e}")
8552 |
8553 | # For each request, try multiple matching strategies
8554 | for req in all_requests:
8555 | # Only consider requests from this conversation
8556 | if req.conversation_id != str(self.context.id):
8557 | continue
8558 |
8559 | # Get string representations of request_id to compare
8560 | req_id_str = str(req.request_id).lower()
8561 | req_id_clean = req_id_str.replace("-", "")
8562 | normalized_id_clean = normalized_id.replace("-", "")
8563 |
8564 | logger.debug(f"Comparing against request: {req_id_str}")
8565 |
8566 | # Multiple matching strategies, from most specific to least
8567 | if any([
8568 | # Exact match
8569 | req_id_str == normalized_id,
8570 | # Match ignoring hyphens
8571 | req_id_clean == normalized_id_clean,
8572 | # Check for UUID format variations
8573 | req_id_str == normalized_id.lower(),
8574 | # Partial match (if one is substring of the other)
8575 | len(normalized_id) >= 6 and normalized_id in req_id_str,
8576 | len(req_id_str) >= 6 and req_id_str in normalized_id,
8577 | # Match on first part of UUID (at least 8 chars)
8578 | len(normalized_id) >= 8 and normalized_id[:8] == req_id_str[:8] and len(req_id_clean) >= 30,
8579 | ]):
8580 | matching_request = req
8581 | break
8582 |
8583 | if matching_request:
8584 | information_request = matching_request
8585 | request_id = matching_request.request_id
8586 | else:
8587 | logger.warning(
8588 | f"Failed deletion attempt - request ID '{request_id}' not found in project {project_id}"
8589 | )
8590 | if available_ids:
8591 | id_examples = ", ".join([f"`{id[:8]}...`" for id in available_ids[:3]])
8592 | return f"Information request with ID '{request_id}' not found. Your available requests have IDs like: {id_examples}. Please check and try again with the exact ID."
8593 | else:
8594 | return f"Information request with ID '{request_id}' not found. You don't have any active requests to delete."
8595 |
8596 | if information_request.conversation_id != str(self.context.id):
8597 | return "You can only delete information requests that you created. This request was created by another conversation."
8598 |
8599 | # Get current user info for logging
8600 | participants = await self.context.get_participants()
8601 | current_user_id = None
8602 | current_username = None
8603 |
8604 | for participant in participants.participants:
8605 | if participant.role == "user":
8606 | current_user_id = participant.id
8607 | current_username = participant.name
8608 | break
8609 |
8610 | if not current_user_id:
8611 | current_user_id = "team-system"
8612 | current_username = "Team Member"
8613 |
8614 | # Log the deletion before removing the request
8615 | request_title = information_request.title
8616 |
8617 | # Store the actual request ID from the information_request object for reliable operations
8618 | actual_request_id = information_request.request_id
8619 |
8620 | # Log the deletion in the project log
8621 | await ProjectStorage.log_project_event(
8622 | context=self.context,
8623 | project_id=project_id,
8624 | entry_type=LogEntryType.REQUEST_DELETED.value,
8625 | message=f"Information request '{request_title}' was deleted by {current_username}",
8626 | related_entity_id=actual_request_id,
8627 | metadata={
8628 | "request_title": request_title,
8629 | "deleted_by": current_user_id,
8630 | "deleted_by_name": current_username,
8631 | },
8632 | )
8633 |
8634 | # Delete the information request - implementing deletion logic by removing the file
8635 | # Using ProjectStorage instead of direct path access
8636 | # Create information requests directory path and remove the specific file
8637 |
8638 | request_path = ProjectStorageManager.get_information_request_path(project_id, actual_request_id)
8639 | if request_path.exists():
8640 | request_path.unlink() # Delete the file
8641 |
8642 | # Notify Coordinator about the deletion
8643 | try:
8644 | # Get Coordinator conversation ID
8645 |
8646 | coordinator_dir = ProjectStorageManager.get_project_dir(project_id) / ConversationRole.COORDINATOR.value
8647 | if coordinator_dir.exists():
8648 | role_file = coordinator_dir / "conversation_role.json"
8649 | if role_file.exists():
8650 | role_data = read_model(role_file, ConversationProjectManager.ConversationRoleInfo)
8651 | if role_data:
8652 | coordinator_conversation_id = role_data.conversation_id
8653 |
8654 | # Notify Coordinator
8655 |
8656 | client = ConversationClientManager.get_conversation_client(
8657 | self.context, coordinator_conversation_id
8658 | )
8659 | await client.send_messages(
8660 | NewConversationMessage(
8661 | content=f"Team member ({current_username}) has deleted their request: '{request_title}'",
8662 | message_type=MessageType.notice,
8663 | )
8664 | )
8665 | except Exception as e:
8666 | logger.warning(f"Could not notify Coordinator about deleted request: {e}")
8667 | # Not critical, so we continue
8668 |
8669 | # Update all project UI inspectors
8670 | await ProjectStorage.refresh_all_project_uis(self.context, project_id)
8671 |
8672 | return f"Information request '{request_title}' has been successfully deleted."
8673 |
8674 | except Exception as e:
8675 | logger.exception(f"Error deleting information request: {e}")
8676 | return f"Error deleting information request: {str(e)}. Please try again later."
8677 |
8678 | async def add_project_goal(self, goal_name: str, goal_description: str, success_criteria: List[str]) -> str:
8679 | """
8680 | Add a goal to the project brief with measurable success criteria.
8681 |
8682 | Project goals should be operational objectives that team members will need to complete.
8683 | Each goal must have clear, measurable success criteria that team members can mark as completed.
8684 |
8685 | WHEN TO USE:
8686 | - When defining actionable goals that team members need to accomplish
8687 | - When breaking down project requirements into specific, achievable objectives
8688 | - After creating a project brief, before marking the project ready for working
8689 | - When users ask to add or define goals, objectives, or tasks for the project
8690 |
8691 | Args:
8692 | goal_name: A concise, clear name for the goal (e.g., "Implement User Authentication")
8693 | goal_description: A detailed description explaining what needs to be accomplished
8694 | success_criteria: List of specific, measurable criteria that indicate when the goal is complete
8695 | (e.g., ["User login form created", "Password reset functionality implemented"])
8696 |
8697 | Returns:
8698 | A message indicating success or failure
8699 | """
8700 |
8701 | if self.role is not ConversationRole.COORDINATOR:
8702 | return "Only Coordinator can add project goals."
8703 |
8704 | # Get project ID
8705 | project_id = await ProjectManager.get_project_id(self.context)
8706 | if not project_id:
8707 | return "No project associated with this conversation. Please create a project brief first."
8708 |
8709 | # Get existing project brief
8710 | brief = await ProjectManager.get_project_brief(self.context)
8711 | if not brief:
8712 | return "No project brief found. Please create one first with create_project_brief."
8713 |
8714 | # Use the formatted command processor from chat.py to leverage existing functionality
8715 | criteria_str = ""
8716 | if len(success_criteria) > 0:
8717 | criteria_str = "|" + ";".join(success_criteria)
8718 |
8719 | command_content = f"/add-goal {goal_name}|{goal_description}{criteria_str}"
8720 |
8721 | return await invoke_command_handler(
8722 | context=self.context,
8723 | command_content=command_content,
8724 | handler_func=handle_add_goal_command,
8725 | success_message=f"Goal '{goal_name}' added to project brief successfully.",
8726 | error_prefix="Error adding goal",
8727 | )
8728 |
8729 | async def delete_project_goal(self, goal_index: int) -> str:
8730 | """
8731 | Delete a goal from the project by index.
8732 |
8733 | WHEN TO USE:
8734 | - When a user explicitly requests to remove or delete a specific project goal
8735 | - When goals need to be reorganized and redundant/obsolete goals removed
8736 | - When a goal was added by mistake or is no longer relevant to the project
8737 | - Only before marking the project as ready for working
8738 |
8739 | NOTE: This action is irreversible and will remove all success criteria associated with the goal.
8740 | First use get_project_info() to see the list of goals and their indices before deletion.
8741 |
8742 | Args:
8743 | goal_index: The index of the goal to delete (0-based integer). Use get_project_info() first to see the
8744 | correct indices of goals. For example, to delete the first goal, use goal_index=0.
8745 |
8746 | Returns:
8747 | A message indicating success or failure
8748 | """
8749 |
8750 | if self.role is not ConversationRole.COORDINATOR:
8751 | return "Only Coordinator can delete project goals."
8752 |
8753 | # Get project ID - validate project exists
8754 | project_id = await ProjectManager.get_project_id(self.context)
8755 | if not project_id:
8756 | return "No project associated with this conversation."
8757 |
8758 | # Call the ProjectManager method to delete the goal
8759 | success, result = await ProjectManager.delete_project_goal(
8760 | context=self.context,
8761 | goal_index=goal_index,
8762 | )
8763 |
8764 | if success:
8765 | # Notify the user about the successful deletion
8766 | await self.context.send_messages(
8767 | NewConversationMessage(
8768 | content=f"Goal '{result}' has been successfully deleted from the project.",
8769 | message_type=MessageType.notice,
8770 | )
8771 | )
8772 | return f"Goal '{result}' has been successfully deleted from the project."
8773 | else:
8774 | # Return the error message
8775 | return f"Error deleting goal: {result}"
8776 |
8777 | async def mark_criterion_completed(self, goal_index: int, criterion_index: int) -> str:
8778 | """
8779 | Mark a success criterion as completed for tracking project progress.
8780 |
8781 | WHEN TO USE:
8782 | - When the user reports completing a specific task or deliverable
8783 | - When evidence has been provided that a success criterion has been met
8784 | - When a milestone for one of the project goals has been achieved
8785 | - When tracking progress and updating the project status
8786 |
8787 | Each completed criterion moves the project closer to completion. When all criteria
8788 | are completed, the project can be marked as complete.
8789 |
8790 | IMPORTANT: Always use get_project_info() first to see the current goals, criteria, and their indices
8791 | before marking anything as complete.
8792 |
8793 | Args:
8794 | goal_index: The index of the goal (0-based integer) from get_project_info() output
8795 | criterion_index: The index of the criterion within the goal (0-based integer)
8796 |
8797 | Returns:
8798 | A message indicating success or failure
8799 | """
8800 |
8801 | if self.role is not ConversationRole.TEAM:
8802 | return "Only Team members can mark criteria as completed."
8803 |
8804 | # Get project ID
8805 | project_id = await ProjectManager.get_project_id(self.context)
8806 | if not project_id:
8807 | return "No project associated with this conversation. Unable to mark criterion as completed."
8808 |
8809 | # Get existing project brief
8810 | brief = await ProjectManager.get_project_brief(self.context)
8811 | if not brief:
8812 | return "No project brief found."
8813 |
8814 | # Using 0-based indexing directly, no adjustment needed
8815 |
8816 | # Get the project to access goals
8817 | project = ProjectStorage.read_project(project_id)
8818 | if not project or not project.goals:
8819 | return "No project goals found."
8820 |
8821 | # Validate indices
8822 | if goal_index < 0 or goal_index >= len(project.goals):
8823 | return f"Invalid goal index {goal_index}. Valid indexes are 0 to {len(project.goals) - 1}. There are {len(project.goals)} goals."
8824 |
8825 | goal = project.goals[goal_index]
8826 |
8827 | if criterion_index < 0 or criterion_index >= len(goal.success_criteria):
8828 | return f"Invalid criterion index {criterion_index}. Valid indexes for goal '{goal.name}' are 0 to {len(goal.success_criteria) - 1}. Goal '{goal.name}' has {len(goal.success_criteria)} criteria."
8829 |
8830 | # Update the criterion
8831 | criterion = goal.success_criteria[criterion_index]
8832 |
8833 | if criterion.completed:
8834 | return f"Criterion '{criterion.description}' is already marked as completed."
8835 |
8836 | # Get current user information
8837 | participants = await self.context.get_participants()
8838 | current_user_id = None
8839 |
8840 | for participant in participants.participants:
8841 | if participant.role == "user":
8842 | current_user_id = participant.id
8843 | break
8844 |
8845 | if not current_user_id:
8846 | return "Could not identify current user."
8847 |
8848 | # Mark as completed
8849 | criterion.completed = True
8850 | criterion.completed_at = datetime.utcnow()
8851 | criterion.completed_by = current_user_id
8852 |
8853 | # Save the updated project with the completed criterion
8854 | ProjectStorage.write_project(project_id, project)
8855 |
8856 | # Log the criterion completion
8857 | await ProjectStorage.log_project_event(
8858 | context=self.context,
8859 | project_id=project_id,
8860 | entry_type=LogEntryType.CRITERION_COMPLETED.value,
8861 | message=f"Success criterion completed: {criterion.description}",
8862 | related_entity_id=None,
8863 | metadata={"goal_name": goal.name, "criterion_description": criterion.description},
8864 | )
8865 |
8866 | # Update project info
8867 | project_info = ProjectStorage.read_project_info(project_id)
8868 |
8869 | if project_info:
8870 | # Count all completed criteria
8871 | completed_criteria = 0
8872 | total_criteria = 0
8873 |
8874 | # Get the project to access goals
8875 | project = ProjectStorage.read_project(project_id)
8876 | if project and project.goals:
8877 | for g in project.goals:
8878 | total_criteria += len(g.success_criteria)
8879 | completed_criteria += sum(1 for c in g.success_criteria if c.completed)
8880 |
8881 | # Update project info with criteria stats
8882 | project_info.completed_criteria = completed_criteria
8883 | project_info.total_criteria = total_criteria
8884 |
8885 | # Calculate progress percentage
8886 | if total_criteria > 0:
8887 | project_info.progress_percentage = int((completed_criteria / total_criteria) * 100)
8888 |
8889 | # Update metadata
8890 | project_info.updated_at = datetime.utcnow()
8891 | project_info.updated_by = current_user_id
8892 | project_info.version += 1
8893 |
8894 | # Save the updated project info
8895 | ProjectStorage.write_project_info(project_id, project_info)
8896 |
8897 | # Notify linked conversations with a message
8898 | await ProjectNotifier.notify_project_update(
8899 | context=self.context,
8900 | project_id=project_id,
8901 | update_type="project_info",
8902 | message=f"Success criterion '{criterion.description}' for goal '{goal.name}' has been marked as completed.",
8903 | )
8904 |
8905 | # Update all project UI inspectors
8906 | await ProjectStorage.refresh_all_project_uis(self.context, project_id)
8907 |
8908 | # Check if all criteria are completed for project completion
8909 | # Count all completed criteria again to check for completion
8910 | completed = 0
8911 | total = 0
8912 |
8913 | # Get the project to access goals
8914 | project = ProjectStorage.read_project(project_id)
8915 | if project and project.goals:
8916 | for g in project.goals:
8917 | total += len(g.success_criteria)
8918 | completed += sum(1 for c in g.success_criteria if c.completed)
8919 |
8920 | if completed == total and total > 0:
8921 | # Automatically complete the project
8922 | success, project_info = await ProjectManager.complete_project(
8923 | context=self.context,
8924 | summary=f"All {total} success criteria have been completed! Project has been automatically marked as complete.",
8925 | )
8926 |
8927 | if success:
8928 | await self.context.send_messages(
8929 | NewConversationMessage(
8930 | content="🎉 All success criteria have been completed! The project has been automatically marked as complete.",
8931 | message_type=MessageType.notice,
8932 | )
8933 | )
8934 | else:
8935 | await self.context.send_messages(
8936 | NewConversationMessage(
8937 | content="🎉 All success criteria have been completed! Would you like me to formally complete the project?",
8938 | message_type=MessageType.notice,
8939 | )
8940 | )
8941 |
8942 | await self.context.send_messages(
8943 | NewConversationMessage(
8944 | content=f"Success criterion '{criterion.description}' for goal '{goal.name}' has been marked as completed.",
8945 | message_type=MessageType.notice,
8946 | )
8947 | )
8948 |
8949 | return f"Criterion '{criterion.description}' for goal '{goal.name}' marked as completed."
8950 |
8951 | async def mark_project_ready_for_working(self) -> str:
8952 | """
8953 | Mark the project as ready for working.
8954 | This is a milestone function that transitions from Planning Stage to Working Stage.
8955 |
8956 | Returns:
8957 | A message indicating success or failure
8958 | """
8959 |
8960 | if self.role is not ConversationRole.COORDINATOR:
8961 | return "Only Coordinator can mark a project as ready for working."
8962 |
8963 | # Get project ID
8964 | project_id = await ProjectManager.get_project_id(self.context)
8965 | if not project_id:
8966 | return "No project associated with this conversation. Unable to mark project as ready for working."
8967 |
8968 | # Get existing project brief, whiteboard, and project
8969 | brief = ProjectStorage.read_project_brief(project_id)
8970 | whiteboard = ProjectStorage.read_project_whiteboard(project_id)
8971 | project = ProjectStorage.read_project(project_id)
8972 |
8973 | if not brief:
8974 | return "No project brief found. Please create one before marking as ready for working."
8975 |
8976 | if not project or not project.goals:
8977 | return "Project has no goals. Please add at least one goal before marking as ready for working."
8978 |
8979 | # Check if at least one goal has success criteria
8980 | has_criteria = False
8981 | for goal in project.goals:
8982 | if goal.success_criteria:
8983 | has_criteria = True
8984 | break
8985 |
8986 | if not has_criteria:
8987 | return "No success criteria defined. Please add at least one success criterion to a goal before marking as ready for working."
8988 |
8989 | # Check if whiteboard has content
8990 | if not whiteboard or not whiteboard.content:
8991 | return "Project whiteboard is empty. Content will be automatically generated as the project progresses."
8992 |
8993 | # Get or create project info
8994 | project_info = ProjectStorage.read_project_info(project_id)
8995 |
8996 | # Get current user information
8997 | participants = await self.context.get_participants()
8998 | current_user_id = None
8999 |
9000 | for participant in participants.participants:
9001 | if participant.role == "user":
9002 | current_user_id = participant.id
9003 | break
9004 |
9005 | if not current_user_id:
9006 | return "Could not identify current user."
9007 |
9008 | if not project_info:
9009 | # Create new project info if it doesn't exist
9010 | project_info = ProjectInfo(
9011 | project_id=project_id,
9012 | coordinator_conversation_id=str(self.context.id),
9013 | state=ProjectState.PLANNING,
9014 | created_at=datetime.utcnow(),
9015 | updated_at=datetime.utcnow(),
9016 | )
9017 |
9018 | # Update state to ready_for_working
9019 | if isinstance(project_info, dict):
9020 | # Handle the dict case for backward compatibility
9021 | project_info["state"] = ProjectState.READY_FOR_WORKING
9022 | project_info["status_message"] = "Project is now ready for team operations"
9023 | project_info["updated_at"] = datetime.utcnow()
9024 | else:
9025 | # Handle the ProjectInfo case
9026 | project_info.state = ProjectState.READY_FOR_WORKING
9027 | project_info.status_message = "Project is now ready for team operations"
9028 | project_info.updated_at = datetime.utcnow()
9029 |
9030 | # Save the updated project info
9031 | ProjectStorage.write_project_info(project_id, project_info)
9032 |
9033 | # Log the milestone transition
9034 | await ProjectStorage.log_project_event(
9035 | context=self.context,
9036 | project_id=project_id,
9037 | entry_type=LogEntryType.MILESTONE_PASSED.value,
9038 | message="Project marked as READY FOR WORKING",
9039 | metadata={"milestone": "ready_for_working"},
9040 | )
9041 |
9042 | # Notify linked conversations with a message
9043 | await ProjectNotifier.notify_project_update(
9044 | context=self.context,
9045 | project_id=project_id,
9046 | update_type="project_info",
9047 | message="🔔 **Project Milestone Reached**: Coordinator has marked the project as READY FOR WORKING. All project information is now available and you can begin team operations.",
9048 | )
9049 |
9050 | # Update all project UI inspectors
9051 | await ProjectStorage.refresh_all_project_uis(self.context, project_id)
9052 |
9053 | await self.context.send_messages(
9054 | NewConversationMessage(
9055 | content="🎯 Project has been marked as READY FOR WORKING. Team members have been notified and can now begin operations.",
9056 | message_type=MessageType.chat,
9057 | )
9058 | )
9059 |
9060 | return "Project successfully marked as ready for team operations."
9061 |
9062 | async def report_project_completion(self) -> str:
9063 | """
9064 | Report that the project is complete, concluding the project lifecycle.
9065 |
9066 | WHEN TO USE:
9067 | - When all success criteria for all goals have been marked as completed
9068 | - When the user confirms the project deliverables are finished and ready
9069 | - When the project objectives have been fully achieved
9070 | - When it's time to formally conclude the project
9071 |
9072 | This is a significant milestone that indicates the project has successfully
9073 | completed all its goals. Before using this tool, verify that all success criteria
9074 | have been marked as completed using get_project_info().
9075 |
9076 | Returns:
9077 | A message indicating success or failure
9078 | """
9079 |
9080 | if self.role is not ConversationRole.TEAM:
9081 | return "Only Team members can report project completion."
9082 |
9083 | # Get project ID
9084 | project_id = await ProjectManager.get_project_id(self.context)
9085 | if not project_id:
9086 | return "No project associated with this conversation. Unable to report project completion."
9087 |
9088 | # Get existing project info
9089 | project_info = ProjectStorage.read_project_info(project_id)
9090 | if not project_info:
9091 | return "No project information found. Cannot complete project without project information."
9092 |
9093 | # Check if all criteria are completed
9094 | if getattr(project_info, "completed_criteria", 0) < getattr(project_info, "total_criteria", 0):
9095 | remaining = project_info.total_criteria - project_info.completed_criteria
9096 | return f"Cannot complete project - {remaining} success criteria are still pending completion."
9097 |
9098 | # Get current user information
9099 | participants = await self.context.get_participants()
9100 | current_user_id = None
9101 |
9102 | for participant in participants.participants:
9103 | if participant.role == "user":
9104 | current_user_id = participant.id
9105 | break
9106 |
9107 | if not current_user_id:
9108 | return "Could not identify current user."
9109 |
9110 | # Update project info to completed
9111 | project_info.state = ProjectState.COMPLETED
9112 | project_info.progress_percentage = 100
9113 | project_info.status_message = "Project is now complete"
9114 |
9115 | # Add lifecycle metadata
9116 | if not hasattr(project_info, "lifecycle") or not project_info.lifecycle:
9117 | project_info.lifecycle = {}
9118 |
9119 | project_info.lifecycle["project_completed"] = True
9120 | project_info.lifecycle["project_completed_time"] = datetime.utcnow().isoformat()
9121 | project_info.lifecycle["project_completed_by"] = current_user_id
9122 |
9123 | # Update metadata
9124 | project_info.updated_at = datetime.utcnow()
9125 | project_info.updated_by = current_user_id
9126 | project_info.version += 1
9127 |
9128 | # Save the updated project info
9129 | ProjectStorage.write_project_info(project_id, project_info)
9130 |
9131 | # Log the milestone transition
9132 | await ProjectStorage.log_project_event(
9133 | context=self.context,
9134 | project_id=project_id,
9135 | entry_type=LogEntryType.PROJECT_COMPLETED.value,
9136 | message="Project marked as COMPLETED",
9137 | metadata={"milestone": "project_completed"},
9138 | )
9139 |
9140 | # Notify linked conversations with a message
9141 | await ProjectNotifier.notify_project_update(
9142 | context=self.context,
9143 | project_id=project_id,
9144 | update_type="project_completed",
9145 | message="🎉 **Project Complete**: Team has reported that all project objectives have been achieved. The project is now complete.",
9146 | )
9147 |
9148 | # Update all project UI inspectors
9149 | await ProjectStorage.refresh_all_project_uis(self.context, project_id)
9150 |
9151 | await self.context.send_messages(
9152 | NewConversationMessage(
9153 | content="🎉 **Project Complete**: All objectives have been achieved and the project is now complete. The Coordinator has been notified.",
9154 | message_type=MessageType.chat,
9155 | )
9156 | )
9157 |
9158 | return "Project successfully marked as complete. All participants have been notified."
9159 |
9160 | async def suggest_next_action(self) -> Dict[str, Any]:
9161 | """
9162 | Suggest the next action the user should take based on project state.
9163 |
9164 | Returns:
9165 | Dict with suggestion details
9166 | """
9167 | # Get project ID
9168 | project_id = await ProjectManager.get_project_id(self.context)
9169 | if not project_id:
9170 | logger.warning("No project ID found for this conversation")
9171 | return {
9172 | "suggestion": "no_project",
9173 | "reason": "No project associated with this conversation. Unable to suggest next action.",
9174 | "priority": "low",
9175 | "function": None,
9176 | }
9177 |
9178 | project_info = ProjectStorage.read_project_info(project_id)
9179 | if not project_info:
9180 | return {
9181 | "suggestion": "no_project_info",
9182 | "reason": "No project information found. Unable to suggest next action.",
9183 | "priority": "low",
9184 | "function": None,
9185 | }
9186 |
9187 | # Get project state information
9188 | brief = ProjectStorage.read_project_brief(project_id)
9189 | project = ProjectStorage.read_project(project_id)
9190 | requests = ProjectStorage.get_all_information_requests(project_id)
9191 |
9192 | # Check if project brief exists
9193 | if not brief:
9194 | if self.role is ConversationRole.COORDINATOR:
9195 | return {
9196 | "suggestion": "create_project_brief",
9197 | "reason": "No project brief found. Start by creating one.",
9198 | "priority": "high",
9199 | "function": "create_project_brief",
9200 | "parameters": {"name": "", "description": ""},
9201 | }
9202 | else:
9203 | return {
9204 | "suggestion": "wait_for_coordinator",
9205 | "reason": "No project brief found. The Coordinator needs to create one before you can proceed.",
9206 | "priority": "medium",
9207 | "function": None,
9208 | }
9209 |
9210 | # Check if goals exist
9211 | if not is_knowledge_transfer_assistant(self.context):
9212 | if not project or not project.goals:
9213 | if self.role is ConversationRole.COORDINATOR:
9214 | return {
9215 | "suggestion": "add_project_goal",
9216 | "reason": "Project has no goals. Add at least one goal with success criteria.",
9217 | "priority": "high",
9218 | "function": "add_project_goal",
9219 | "parameters": {"goal_name": "", "goal_description": "", "success_criteria": []},
9220 | }
9221 | else:
9222 | return {
9223 | "suggestion": "wait_for_goals",
9224 | "reason": "Project has no goals. The Coordinator needs to add goals before you can proceed.",
9225 | "priority": "medium",
9226 | "function": None,
9227 | }
9228 |
9229 | # Check project info if project is ready for working
9230 | ready_for_working = project_info.state == ProjectState.READY_FOR_WORKING
9231 |
9232 | if not ready_for_working and self.role is ConversationRole.COORDINATOR:
9233 | # Check if it's ready to mark as ready for working
9234 | if not is_knowledge_transfer_assistant(self.context):
9235 | has_goals = True
9236 | has_criteria = True
9237 | else:
9238 | has_goals = bool(project and project.goals)
9239 | has_criteria = bool(
9240 | project and project.goals and any(bool(goal.success_criteria) for goal in project.goals)
9241 | )
9242 |
9243 | if has_goals and has_criteria:
9244 | return {
9245 | "suggestion": "mark_ready_for_working",
9246 | "reason": "Project information is complete. Mark it as ready for team operations.",
9247 | "priority": "medium",
9248 | "function": "mark_project_ready_for_working",
9249 | "parameters": {},
9250 | }
9251 |
9252 | # Check for unresolved information requests for Coordinator
9253 | if self.role is ConversationRole.COORDINATOR:
9254 | active_requests = [r for r in requests if r.status == RequestStatus.NEW]
9255 | if active_requests:
9256 | request = active_requests[0] # Get the first unresolved request
9257 | return {
9258 | "suggestion": "resolve_information_request",
9259 | "reason": f"There are {len(active_requests)} unresolved information requests. Consider resolving '{request.title}'.",
9260 | "priority": "high"
9261 | if request.priority in [RequestPriority.HIGH, RequestPriority.CRITICAL]
9262 | else "medium",
9263 | "function": "resolve_information_request",
9264 | "parameters": {"request_id": request.request_id, "resolution": ""},
9265 | }
9266 |
9267 | # For team, check if all criteria are completed for project completion
9268 | criteria = await ProjectManager.get_project_criteria(self.context)
9269 | incomplete_criteria = [criterion for criterion in criteria if not criterion.completed]
9270 |
9271 | if self.role is ConversationRole.TEAM and not incomplete_criteria:
9272 | return {
9273 | "suggestion": "report_project_completion",
9274 | "reason": "All success criteria have been completed. Report project completion.",
9275 | "priority": "medium",
9276 | "function": "report_project_completion",
9277 | "parameters": {},
9278 | }
9279 |
9280 | # For team, suggest marking criteria as completed if any are pending
9281 | if self.role is ConversationRole.TEAM and incomplete_criteria:
9282 | # Get the project to access goals
9283 | project = ProjectStorage.read_project(project_id)
9284 | if project and project.goals:
9285 | # Find the first uncompleted criterion
9286 | for goal_index, goal in enumerate(project.goals):
9287 | for criterion_index, criterion in enumerate(goal.success_criteria):
9288 | if not criterion.completed:
9289 | return {
9290 | "suggestion": "mark_criterion_completed",
9291 | "reason": "Update progress by marking completed success criteria.",
9292 | "priority": "low",
9293 | "function": "mark_criterion_completed",
9294 | "parameters": {
9295 | "goal_index": goal_index, # 0-based indexing
9296 | "criterion_index": criterion_index, # 0-based indexing
9297 | },
9298 | }
9299 |
9300 | # Default suggestions based on role
9301 | if self.role is ConversationRole.COORDINATOR:
9302 | return {
9303 | "suggestion": "monitor_progress",
9304 | "reason": "Monitor team operations and respond to any new information requests.",
9305 | "priority": "low",
9306 | "function": None,
9307 | }
9308 | else:
9309 | return {
9310 | "suggestion": "update_status",
9311 | "reason": "Continue team operations and update project progress as you make advancements.",
9312 | "priority": "low",
9313 | "function": "update_project_info",
9314 | "parameters": {"status": "in_progress"},
9315 | }
9316 |
9317 |
9318 | === File: assistants/project-assistant/assistant/utils.py ===
9319 | """
9320 | Utility functions for the project assistant module.
9321 |
9322 | This module provides common utility functions used across the project assistant
9323 | codebase, helping to reduce code duplication and maintain consistency.
9324 | """
9325 |
9326 | import pathlib
9327 | from enum import Enum
9328 | from typing import Optional, Tuple
9329 |
9330 | from semantic_workbench_assistant.assistant_app import ConversationContext
9331 |
9332 | from .logging import logger
9333 |
9334 | KNOWLEDGE_TRANSFER_TEMPLATE_ID = "knowledge_transfer"
9335 | DEFAULT_TEMPLATE_ID = "default"
9336 |
9337 |
9338 | class ConfigurationTemplate(Enum):
9339 | """
9340 | This assistant can be in one of two different template configurations. It
9341 | behaves quite differently based on which configuration it it in.
9342 | """
9343 |
9344 | PROJECT_ASSISTANT = DEFAULT_TEMPLATE_ID
9345 | KNOWLEDGE_TRANSFER_ASSISTANT = KNOWLEDGE_TRANSFER_TEMPLATE_ID
9346 |
9347 |
9348 | def get_template(context: ConversationContext) -> ConfigurationTemplate:
9349 | template_id = context.assistant._template_id or DEFAULT_TEMPLATE_ID
9350 | return (
9351 | ConfigurationTemplate.PROJECT_ASSISTANT
9352 | if template_id == DEFAULT_TEMPLATE_ID
9353 | else ConfigurationTemplate.KNOWLEDGE_TRANSFER_ASSISTANT
9354 | )
9355 |
9356 |
9357 | def is_knowledge_transfer_assistant(context: ConversationContext) -> bool:
9358 | """
9359 | Determine if the assistant is using the context transfer template.
9360 | """
9361 | return context.assistant._template_id == KNOWLEDGE_TRANSFER_TEMPLATE_ID
9362 |
9363 |
9364 | def load_text_include(filename) -> str:
9365 | """
9366 | Helper for loading an include from a text file.
9367 |
9368 | Args:
9369 | filename: The name of the text file to load from the text_includes directory
9370 |
9371 | Returns:
9372 | The content of the text file
9373 | """
9374 | # Get directory relative to this module
9375 | directory = pathlib.Path(__file__).parent
9376 |
9377 | # Get the file path for the prompt file
9378 | file_path = directory / "text_includes" / filename
9379 |
9380 | # Read the prompt from the file
9381 | return file_path.read_text()
9382 |
9383 |
9384 | async def get_current_user(context: ConversationContext) -> Tuple[Optional[str], Optional[str]]:
9385 | """
9386 | Extract the current user ID and name from the conversation context.
9387 |
9388 | Args:
9389 | context: The conversation context to extract user information from
9390 |
9391 | Returns:
9392 | A tuple containing (user_id, user_name), both may be None if no user is found
9393 | """
9394 | participants = await context.get_participants()
9395 | user_id = None
9396 | user_name = None
9397 |
9398 | for participant in participants.participants:
9399 | if participant.role == "user":
9400 | user_id = participant.id
9401 | user_name = participant.name
9402 | break
9403 |
9404 | return user_id, user_name
9405 |
9406 |
9407 | async def get_current_user_id(context: ConversationContext) -> Optional[str]:
9408 | """
9409 | Extract just the current user ID from the conversation context.
9410 |
9411 | Args:
9412 | context: The conversation context to extract user information from
9413 |
9414 | Returns:
9415 | The user ID, or None if no user is found
9416 | """
9417 | user_id, _ = await get_current_user(context)
9418 | return user_id
9419 |
9420 |
9421 | async def require_current_user(context: ConversationContext, operation_name: str) -> Optional[str]:
9422 | """
9423 | Extract the current user ID and log an error if none is found.
9424 |
9425 | Args:
9426 | context: The conversation context to extract user information from
9427 | operation_name: Name of the operation requiring a user, for error logging
9428 |
9429 | Returns:
9430 | The user ID, or None if no user is found (after logging an error)
9431 | """
9432 | user_id = await get_current_user_id(context)
9433 |
9434 | if not user_id:
9435 | logger.error(f"Cannot {operation_name}: no user found in conversation")
9436 |
9437 | return user_id
9438 |
9439 |
9440 | === File: assistants/project-assistant/docs/ASSISTANT_LIBRARY_NOTES.md ===
9441 | # Semantic Workbench Assistant Library
9442 |
9443 | This document provides an overview of the Semantic Workbench Assistant library found in `/workspaces/semanticworkbench/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant`.
9444 |
9445 | ## Overview
9446 |
9447 | The semantic-workbench-assistant library provides a framework for building assistants that integrate with the Semantic Workbench platform. It handles communication protocols, state management, event handling, and provides an abstraction layer that simplifies assistant development.
9448 |
9449 | ## Key Components
9450 |
9451 | ### Core Classes
9452 |
9453 | - **AssistantApp**: The main entry point for creating assistants with event subscriptions.
9454 | - Configures service metadata, capabilities, and providers
9455 | - Initializes event handling system
9456 | - Creates the FastAPI application
9457 |
9458 | - **ConversationContext**: Interface for interacting with conversations
9459 | - Manages messages, files, and conversation state
9460 | - Provides methods for conversation operations
9461 | - Integrates with the Workbench client for API operations
9462 |
9463 | - **AssistantContext**: Holds information about the assistant identity
9464 |
9465 | ### State Management
9466 |
9467 | - **File-based persistence**:
9468 | - `storage.py` provides `read_model()` and `write_model()` for serializing Pydantic models to files
9469 | - Models are stored as JSON in a configurable storage directory
9470 | - `storage_directory_for_context()` creates unique paths for each assistant/conversation
9471 |
9472 | - **Context objects**:
9473 | - `AssistantContext` and `ConversationContext` serve as state containers
9474 | - Conversation-specific operations are accessed through the context
9475 |
9476 | ### Communication
9477 |
9478 | - **Event-driven architecture**:
9479 | - Assistants subscribe to conversation events (messages, participants, files)
9480 | - Asynchronous event processing queues decouple event reception from handling
9481 | - Event system notifies the assistant of changes to track state
9482 |
9483 | - **API Integration**:
9484 | - Uses a client to communicate with the Workbench service
9485 | - Provides methods for sending messages, managing files, and updating state
9486 |
9487 | ## Usage for Project Assistant
9488 |
9489 | For the Project Assistant, the library provides:
9490 |
9491 | 1. **File storage mechanisms** for persisting project state between sessions
9492 | 2. **Context objects** for accessing conversation data and performing operations
9493 | 3. **Event handling** for reacting to changes in conversations
9494 | 4. **Cross-conversation capabilities** through API clients
9495 | 5. **Abstraction layer** for Workbench service integration
9496 |
9497 | ## Implementation Details
9498 |
9499 | ### Storage Pattern
9500 |
9501 | ```python
9502 | # Example of reading/writing models
9503 | from semantic_workbench_assistant.storage import read_model, write_model
9504 |
9505 | # Write a model to storage
9506 | def save_state(context, data):
9507 | path = storage_directory_for_context(context) / "project_data.json"
9508 | write_model(path, data)
9509 |
9510 | # Read a model from storage
9511 | def load_state(context, model_class):
9512 | path = storage_directory_for_context(context) / "project_data.json"
9513 | return read_model(path, model_class)
9514 | ```
9515 |
9516 | ### Context Usage
9517 |
9518 | ```python
9519 | # Example of context methods
9520 | async def process_message(context: ConversationContext, message):
9521 | # Send a response
9522 | await context.send_messages(NewConversationMessage(
9523 | content="Processing your request",
9524 | message_type=MessageType.chat
9525 | ))
9526 |
9527 | # Access files
9528 | files = await context.get_files()
9529 |
9530 | # Update conversation state
9531 | await context.send_conversation_state_event(
9532 | AssistantStateEvent(state_id="project_dashboard", event="updated", state=None)
9533 | )
9534 | ```
9535 |
9536 | === File: assistants/project-assistant/docs/CLAUDE_PROMPTS.md ===
9537 | - Let's keep working on the project-assistant project (assistants/project-assistant). Please reread the notes in docs/DESIGN.md for the big picture. Also, as we work on code, please respect the docs/DEV_GUIDE.md and check out some notes on relevant libraries at docs/ASSISTANT_LIBRARY_NOTES.md, and docs/WORKBENCH_NOTES.md. Let me know when you've brushed up on all that and I'll let you know what we're working on next.
9538 | - Feel free to look for examples in other assistants we've been developing in the assistants directory. assistants/codespace-assistant is more up-to-date than the rest.
9539 | - I would like your help in creating what I call a "Plan of Action". This is different than a normal planning document in that it is a set of instructions that, if followed by a software developer, would result in the plan's objectives being met. A POA is created by investigating relevant code including the current project source as well as library code that is being imported and third-party code when necessary. This investigation needs to occur because each POA step should be very specific, such as "delete <some file>" or "use <function in library> to implement <method>" or "create new module that operates over <library y> methods". POAs will be reviewed by senior developers, and you can expect that they will call you out if you are not using existing library methods to do things or have an overly-complicated or too-simple plan. For this reason, make sure that your plan is well researched and well reasoned. Let's start a POA in docs/POA.md for this: Instead of using /start and /join commands to link conversations, we want to look into using the cloning approach to create a new conversation seeded/configured for the recipient users and then create copy/read-only share links that your assistant could provide back (or put into your inspector) to give to the other users - then they would just follow those share links using the normal redemption approach we use for all shares, which would create a copy of that newly created conversation for each user, and those could be associated with the group (whatever term was landed on for that) but since they are not the originator, they'd all be in whatever mode /join puts them in. Current cloning code includes conversations and other things we don't need for a new connected conversation, so we don't want to use it directly, instead, find what parts you wanted - which is likely mostly creating the conversation, the assistant/config, and a welcome message. For this plan, you should be familiar with _at least_ the workbench service controller methods (API) that can be found at /home/payne/repos/semanticworkbench/workbench-service/semantic_workbench_service/controller, and the clients that work with this API that you have access to that can be found at /home/payne/repos/semanticworkbench/libraries/python/semantic-workbench-api-model/semantic_workbench_api_model and our assistant base class that is found at /home/payne/repos/semanticworkbench/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant.
9540 | - are you aware we can get a ConversationAPIClient from the ConversationContext of the current conversation (available in all chat event handlers). See:
9541 | /home/payne/repos/semanticworkbench/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py
9542 | - Excellent. Now, let's work on copy. Note, we want to have different copy in some places depending on whether the assistant is using the "context transfer" config template, or the "default" (project) config template, so any time
9543 | │ we have a copy difference between the two templates, the copy should be located in config.py.
9544 | - Let's keep working on the project-assistant project (assistants/project-assistant). Please reread the notes in docs/DESIGN.md for the big picture. Also, as we work on code, please respect the docs/DEV_GUIDE.md and check out some notes on relevant libraries at docs/ASSISTANT_LIBRARY_NOTES.md, and docs/WORKBENCH_NOTES.md. I have recorded our most recent work in docs/WORKING.md. Once you have read all those, let me know and I'll tell you what we're working on now.
9545 | - You have access to logs in .data/logs. We're trying to fix the missing welcome message after a share link is redeemed. You can see what code we've changed using git recently to add more logging. Check out the code and logs and help me figure out why welcome messages aren't getting added on shared team conversations.
9546 |
9547 |
9548 | === File: assistants/project-assistant/docs/DESIGN.md ===
9549 | # Project Assistant Implementation Plan
9550 |
9551 | ## Overview
9552 |
9553 | The Project Assistant is designed as a dual-role context transfer system that facilitates knowledge transfer between different conversations in the Semantic Workbench. It provides a structured way for Coordinators to create project information and Team members to access it, with bidirectional updates and communication.
9554 |
9555 | ## System Design
9556 |
9557 | ### Configuration Templates
9558 |
9559 | The Project Assistant supports two different configuration templates within a unified codebase. The templates modify the behavior of the assistants considerably, so we basically have two different assistants with this single assistant codebase.
9560 |
9561 | 1. **Default Template (Project Assistant)**:
9562 | - Full project management capabilities with tracking features
9563 | - Includes goals, success criteria, and project state monitoring
9564 | - Tracks progress of project tasks and completion status
9565 | - Uses "Project Assistant" branding and terminology
9566 | - Provides comprehensive project dashboard with status tracking
9567 | - Focuses on project management workflow with sequential stages
9568 |
9569 | 2. **Context Transfer Template (Context Transfer Assistant)**:
9570 | - Simplified knowledge sharing without project tracking features
9571 | - Designed for knowledge transfer without formal project structure
9572 | - No goals or success criteria tracking
9573 | - Uses "Context Transfer Assistant" branding and terminology
9574 | - Maintains core information request capabilities without project stages
9575 | - Different welcome messages and UI labels appropriate for knowledge sharing
9576 |
9577 | The system automatically adapts its behavior, prompts, and UI based on which template is selected during assistant instantiation, while maintaining a unified codebase and consistent architecture.
9578 |
9579 | Some features remain in both assistant configurations:
9580 |
9581 | - Both assistants maintain a "whiteboard" internally to build up context of the project (the "project" for the Context Transfer Assistant is to transfer the context well, while for the Project Assistant it is a project in a more formal sense). The whiteboard is updated by the assistant each time they reply to the user. The whiteboard is distinct from the project information dashboard which is a collaboration between the user and the assistant.
9582 | - Both assistants syncronize project information and files between coordinators and team members.
9583 | - Both assistants facilitate the creation of information requests by team members, and the answering of information requests by the coordinators.
9584 | - Both assistants can speak conversationally and run their own sets of tools.
9585 |
9586 | ### Conversation Structure
9587 |
9588 | The Project Assistant (in both configurations) manages three distinct types of conversations:
9589 |
9590 | 1. **Coordinator Conversation**:
9591 | - Created when a user first interacts with the Project Assistant
9592 | - Acts as the personal workspace for the project owner/coordinator
9593 | - Contains private communication between the coordinator and the assistant
9594 | - Stores the link to the shareable team conversation
9595 | - Used for high-level project management and planning
9596 |
9597 | 2. **Shareable Team Conversation**:
9598 | - Automatically created when a coordinator starts a new project
9599 | - Never directly used by any user - serves as a template only
9600 | - Has a share URL associated with it
9601 | - When team members click the share link, they get a copy of this conversation
9602 | - Contains project-specific setup and metadata
9603 |
9604 | 3. **Team Conversation(s)**:
9605 | - Created when a team member redeems the share URL
9606 | - Each team member gets their own personal conversation
9607 | - All team conversations are linked to the same project
9608 | - Used for team members to work on the project, make information requests, etc.
9609 | - Automatically set up with the team member role
9610 |
9611 | ### Conversation Roles
9612 |
9613 | Within each configuration template, the Project Assistant supports two distinct conversation roles:
9614 |
9615 | 1. **Coordinator Role**:
9616 | - Knowledge Base Development: Collection and organization of project-critical information
9617 | - Information Request Resolution: Coordinator resolves information requests from team members
9618 | - **In Default Template**:
9619 | - Project Brief Creation with goals and success criteria
9620 | - Project Preparation with staged milestones
9621 | - "Ready for Working" milestone management
9622 | - **In Context Transfer Template**:
9623 | - Knowledge organization without formal project stages
9624 | - Focus on information structuring without tracking progress
9625 |
9626 | 2. **Team Member Role**:
9627 | - Information Access: Team members interact with the shared Knowledge Base
9628 | - Request Management: Team members create and delete information requests as needed
9629 | - **In Default Template**:
9630 | - Progress Tracking with criteria completion
9631 | - Dashboard updates with completion status
9632 | - Support for "Project Completion" milestone
9633 | - **In Context Transfer Template**:
9634 | - Knowledge exploration without progress tracking
9635 | - Information requests without formal project stages
9636 |
9637 | For both configuration templates, the system supports an iterative and asynchronous workflow where the team members' operations and the coordinator's support activities can occur concurrently. The default template provides a comprehensive project dashboard with status tracking, while the context transfer template focuses on knowledge exchange without formal project stages.
9638 |
9639 | ### Key Architectural Features
9640 |
9641 | 1. **Simplified Invitation System**:
9642 | - Uses Semantic Workbench features to clone new team conversations from shareable conversations. The coordinator gives the team members redeemable share links out-of-band.
9643 |
9644 | 2. **Comprehensive LLM Context**:
9645 | - Project data (brief, info, whiteboard, requests) embedded directly in prompts
9646 | - Role-specific formatting to highlight relevant information
9647 | - Dynamic listing of information requests with proper ID formatting
9648 | - Intelligent truncation to manage context length
9649 | - Improves response quality by reducing the need for tool calls
9650 |
9651 | 3. **Robust Information Request Management**:
9652 | - Complete lifecycle from creation to deletion
9653 | - Enhanced UUID handling with multiple matching strategies
9654 | - Conversation-based ownership controls
9655 | - Role-appropriate visibility of requests
9656 | - Proper notification for all parties
9657 |
9658 | 4. **Coordinator Conversation Sharing**:
9659 | - Selective Coordinator conversation message sharing with team members
9660 | - Storage in a centralized JSON file for all team members to access
9661 | - Automatic capture of both user and assistant messages
9662 | - Metadata preservation including sender name and timestamp
9663 | - Limited to recent messages (last 50) to prevent excessive storage
9664 | - Team members can view Coordinator discussions for better context awareness
9665 |
9666 | ## Data
9667 |
9668 | The Project Assistant manages several key entities that work together to provide a cohesive experience:
9669 |
9670 | 1. **Project Brief**: A clear, concise statement of the project, including goals, success criteria, and high-level context necessary for the Team to start. Owned by Coordinator, with individual success criteria that can be marked complete by Team members.
9671 |
9672 | 2. **Project Whiteboard**: A dynamic, automatically updated knowledge repository that captures key information from conversations. The whiteboard is continuously updated as the coordinator interacts with the assistant, extracting and organizing essential project context. It's formatted in Markdown and made available to all team members without requiring manual maintenance.
9673 |
9674 | 3. **Project Info**: Core metadata and state information about the project, including its unique ID, name, current lifecycle state, conversation IDs, share URL, and status messages. This model serves as the central reference point for project identification and collaboration settings. It replaced the previous dashboard entity to eliminate duplication and provide cleaner separation between data and UI.
9675 |
9676 | 4. **Information Requests**: A concise, prioritized list of Team needs—specifically unresolved blockers, missing information, or required resources—logged for Coordinator review and resolution. Created and deletable by Team members, resolved by Coordinator, with robust UUID-based identification.
9677 |
9678 | 5. **Project Log**: A chronological record of all actions and interactions during the project, including updates to the project whiteboard, creation and deletion of information requests, and progress reports from the team. Maintained by the system, visible to both coordinator and team members.
9679 |
9680 | 6. **Coordinator Conversation Storage**: A selective representation of key Coordinator conversation messages made accessible to Team members for context. Includes both user and assistant messages with metadata, limited to the most recent 50 messages to prevent excessive storage growth, with proper attribution of message sources.
9681 |
9682 | The State Inspector UI component (visible tab in the Semantic Workbench) dynamically composes information from these entities to present a unified view, rather than relying on a single "dashboard" entity. This decoupling of data from UI allows for more flexible presentation and eliminates redundancy.
9683 |
9684 | ## Storage Architecture
9685 |
9686 | The Project Assistant leverages the Semantic Workbench Assistant library's storage capabilities to maintain project state and artifacts. The storage architecture is structured as follows:
9687 |
9688 | ```
9689 | projects/
9690 | ├── project_id_1/
9691 | │ ├── linked_conversations/ # Directory tracking all linked conversations
9692 | │ │ ├── conversation_id_1 # Empty file - just presence indicates linkage
9693 | │ │ ├── conversation_id_2 # Empty file for another linked conversation
9694 | │ │ └── ... # One file per linked conversation
9695 | │ ├── requests/ # Information requests directory
9696 | │ │ ├── request_id_1.json # Individual request files
9697 | │ │ └── request_id_2.json # Each with a unique UUID
9698 | │ ├── project.json # Core project information
9699 | │ ├── brief.json # Brief data
9700 | │ ├── whiteboard.json # Automatically updated knowledge content
9701 | │ ├── log.json # Chronological event log
9702 | │ └── coordinator_conversation.json # Recent coordinator messages for team access
9703 | └── project_id_2/
9704 | └── ...
9705 | ```
9706 |
9707 | Additionally, conversation-specific data is stored in the assistant library's context-specific storage. This provides the mechanism for the assistant to know which project it is a part of:
9708 |
9709 | ```
9710 | .data/assistants/{assistant_id}/conversations/{conversation_id}/
9711 | ├── project_role.json # Role of this conversation (coordinator or team)
9712 | └── project_association.json # Project this conversation is associated with
9713 | ```
9714 |
9715 | Key implementation details:
9716 |
9717 | - Using the assistant library's `storage_directory_for_context()` to generate unique storage paths
9718 | - Storing Pydantic models via the library's `read_model()` and `write_model()` functions
9719 | - Each project gets a unique folder containing all shared project data
9720 | - Conversation roles and project associations tracked in conversation-specific storage
9721 | - Linked conversations tracked with empty files in a special directory
9722 | - Information requests stored as individual files with UUID-based filenames
9723 | - Auto-updating whiteboard maintained with AI-processed conversation content
9724 | - Coordinator conversation messages stored centrally with a maximum of 50 recent messages
9725 | - Project log maintained as a single growing JSON file with chronological entries
9726 | - Clean separation between conversation-specific and shared project data
9727 |
9728 | ## Role-Based Behavior
9729 |
9730 | The project assistant provides a customized experience based on whether the user is in Coordinator or Team mode:
9731 |
9732 | ### Coordinator Role
9733 |
9734 | - Creates and updates the Project Brief with goals and success criteria
9735 | - Contributes to the auto-updating Project Whiteboard through conversations
9736 | - Shares a unique project URL with team members for easy access
9737 | - Receives and resolves Information Requests from team members
9738 | - Sees all active requests from all team members with their priorities
9739 | - Controls milestone transitions (in default template)
9740 | - Receives notifications when team members delete their requests
9741 | - Gets comprehensive project data directly in the LLM prompt context
9742 |
9743 | ### Team Member Role
9744 |
9745 | - Works with the Project Whiteboard that's automatically updated based on coordinator conversations
9746 | - Creates Information Requests when encountering information gaps or blockers
9747 | - Deletes Information Requests that are no longer needed
9748 | - Joins projects by redeeming the share URL provided by the coordinator
9749 | - Views requests from other team members in read-only mode
9750 | - Reports on progress and findings
9751 | - Marks individual success criteria as completed (in default template)
9752 | - Views recent coordinator conversations for additional context
9753 | - Gets comprehensive project data directly in the LLM prompt context
9754 |
9755 | ## Data Models
9756 |
9757 | Five key entity types provide the foundation for project communication:
9758 |
9759 | 1. **Project Brief**:
9760 | - Project name and description
9761 | - Goals with priority levels
9762 | - Success criteria with completion tracking
9763 | - Individual criterion completion with timestamp and attribution
9764 | - Version tracking for modifications
9765 |
9766 | 2. **Project Whiteboard**:
9767 | - Dynamically generated and auto-updated content
9768 | - AI-powered synthesis of conversation content
9769 | - Simplified Markdown formatting for readability
9770 | - Version tracking with timestamps
9771 | - Automatic organization of key information
9772 | - Content truncation to maintain manageable size (limited to ~2000 tokens)
9773 | - Updated after each assistant message in coordinator conversations
9774 |
9775 | 3. **Project Info**:
9776 | - Unique project identifier and name
9777 | - Current project state tracking (planning, ready_for_working, in_progress, completed, aborted)
9778 | - Coordinator and team conversation IDs
9779 | - Shareable invitation URL for team members
9780 | - Status messages and custom project notes
9781 | - Creation and update timestamps
9782 | - Serves as the central reference for project metadata
9783 | - Replaced previous dashboard entity to decouple data from UI
9784 |
9785 | 4. **Information Requests**:
9786 | - Prioritized information needs (low, medium, high, critical)
9787 | - Status tracking (new, acknowledged, in_progress, resolved, deferred)
9788 | - Complete request lifecycle (creation, deletion)
9789 | - UUID-based identification with flexible matching
9790 | - Resolution information and update history
9791 | - Conversation-based ownership controls
9792 |
9793 | 5. **Project Log**:
9794 | - Chronological record of all events
9795 | - Rich categorization system with specialized entry types
9796 | - Attribution of actions to specific users
9797 | - Metadata for event context
9798 | - Events for milestone transitions, request handling, and whiteboard updates
9799 | - Request deletion tracking
9800 | - Full history of all project activities
9801 |
9802 | 6. **Coordinator Conversation Storage**:
9803 | - Shared access to coordinator conversation for team members
9804 | - Content and sender metadata preservation
9805 | - Limited message history (most recent 50 messages)
9806 | - Automatic pruning of older messages
9807 | - Includes both user and assistant messages
9808 |
9809 |
9810 | === File: assistants/project-assistant/docs/DEV_GUIDE.md ===
9811 | # Coding Guidelines for Project Assistant
9812 |
9813 | This section outlines the core implementation philosophy and guidelines for all code in this code base. It serves as a central reference for decision-making and development approach throughout the project.
9814 |
9815 | ## Core Philosophy
9816 |
9817 | - **Wabi-sabi philosophy**: Embracing simplicity and the essential. Each line serves a clear purpose without unnecessary embellishment.
9818 | - **Occam's Razor thinking**: The solution should be as simple as possible, but no simpler.
9819 | - **Trust in emergence**: Complex systems work best when built from simple, well-defined components that do one thing well.
9820 | - **Present-moment focus**: The code handles what's needed now rather than anticipating every possible future scenario.
9821 | - **Pragmatic trust**: The developer trusts external systems enough to interact with them directly, handling failures as they occur rather than assuming they'll happen.
9822 |
9823 | This developer likely values clear documentation, readable code, and believes good architecture emerges from simplicity rather than being imposed through complexity.
9824 |
9825 | ## Design Guidelines
9826 |
9827 | ### 1. Ruthless Simplicity
9828 |
9829 | - **KISS principle taken to heart**: Keep everything as simple as possible, but no simpler
9830 | - **Minimize abstractions**: Every layer of abstraction must justify its existence
9831 | - **Start minimal, grow as needed**: Begin with the simplest implementation that meets current needs
9832 | - **Avoid future-proofing**: Don't build for hypothetical future requirements
9833 | - **Question everything**: Regularly challenge complexity in the codebase
9834 |
9835 | ### 2. Architectural Integrity with Minimal Implementation
9836 |
9837 | - **Preserve key architectural patterns**: Follow existing patterns when implementing new features.
9838 | - **Simplify implementations**: Maintain pattern benefits with dramatically simpler code.
9839 | - **Scrappy but structured**: Lightweight implementations of solid architectural foundations.
9840 | - **End-to-end thinking**: Focus on complete flows rather than perfect components.
9841 |
9842 | ### 3. Library Usage Philosophy
9843 |
9844 | - **Use libraries as intended**: Minimal wrappers around external libraries.
9845 | - **Direct integration**: Avoid unnecessary adapter layers.
9846 | - **Selective dependency**: Add dependencies only when they provide substantial value.
9847 | - **Understand what you import**: No black-box dependencies.
9848 |
9849 | ## Architectural Guidelines
9850 |
9851 | ### API Layer
9852 |
9853 | - Implement only essential endpoints.
9854 | - Minimal middleware with focused validation.
9855 | - Clear error responses with useful messages.
9856 | - Consistent patterns across endpoints.
9857 |
9858 | ### Database & Storage
9859 |
9860 | - Simple schema focused on current needs.
9861 | - Use TEXT/JSON fields to avoid excessive normalization early.
9862 | - Add indexes only when needed for performance.
9863 | - Delay complex database features until required.
9864 |
9865 | ## Coding Guidelines
9866 |
9867 | ### Remember
9868 |
9869 | - It's easier to add complexity later than to remove it.
9870 | - Code you don't write has no bugs.
9871 | - Favor clarity over cleverness.
9872 | - The best code is often the simplest.
9873 | - Follow existing patterns when implementing new features.
9874 |
9875 | ### Code Style
9876 |
9877 | Follow the project's established code style for consistency:
9878 |
9879 | - Use 4 spaces for indentation
9880 | - Maximum line length is 120 characters
9881 | - Follow PEP 8 naming conventions
9882 | - Use type annotations consistently
9883 | - Write docstrings for functions and classes
9884 |
9885 | ### Quality Checks
9886 |
9887 | You can generally check that code works by running `make lint && make type-check && make test` from the project directory you have made changes in.
9888 |
9889 | #### Linting
9890 |
9891 | Run the linter to check for code quality issues:
9892 |
9893 | ```bash
9894 | make lint
9895 | ```
9896 |
9897 | #### Type Checking
9898 |
9899 | To check for type and compilation issues, use the following command:
9900 |
9901 | ```bash
9902 | make type-check
9903 | ```
9904 |
9905 | Don't try to "get around" type-check problems. Solve them with proper type handling.
9906 |
9907 | Note: Type checking might report errors related to imports from external dependencies. These are expected in development but should be resolved before deployment.
9908 |
9909 | ##### Common Type Issues
9910 |
9911 | - **Parameter name mismatch**: Ensure parameter names match between function declarations and calls
9912 | - **Missing imports**: Import necessary types from their source modules.
9913 | - **Attribute access**: Only check that attributes exist on dynamicly-typed objects. Use strong typing when possible.
9914 | - **Type compatibility**: Ensure assigned values match the expected type (e.g., string vs enum).
9915 |
9916 | #### Testing
9917 |
9918 | Run tests to verify functionality:
9919 |
9920 | ```bash
9921 | make test
9922 | ```
9923 |
9924 | For a specific test file:
9925 |
9926 | ```bash
9927 | uv run python -m pytest tests/test_file.py -v
9928 | ```
9929 |
9930 | ### Important Development Tips that junior devs often get wrong
9931 |
9932 | - Keep the ProjectState enum consistent across all files that use it.
9933 | - When modifying model attributes, update all references across the codebase.
9934 | - Use Optional typing for parameters that might be None.
9935 | - Import Management:
9936 | - Always place imports at the top of the file, organized by stdlib, third-party, and local imports. Keep import statements clean and well-organized to improve code readability
9937 | - - Never use imports inside functions - if a circular dependency exists, use TYPE_CHECKING from the typing module:
9938 |
9939 | ```python
9940 | from typing import TYPE_CHECKING
9941 | if TYPE_CHECKING:
9942 | from .module import Class # Import only used for type hints
9943 | ```
9944 |
9945 | - Update tests when changing functionality.
9946 | - Do not remove tests unless the functionality they test has also been removed. Never skip tests.
9947 | - If you want to run python scripts, you MUST use `uv` from the project directory for them to be in the correct environment.
9948 | - You have access to logs in a project's .data/logs directory. They have timestamps in the name, so the latests logs are sorted last.
9949 | - Never make any git commits. The QA will do that after they review your code.
9950 |
9951 |
9952 | === File: assistants/project-assistant/docs/WORKBENCH_NOTES.md ===
9953 | # Semantic Workbench State Management
9954 |
9955 | This document provides an overview of the state management facilities available in the Semantic Workbench ecosystem that can be leveraged by assistants.
9956 |
9957 | ## Architectural Overview
9958 |
9959 | ```mermaid
9960 | graph TD
9961 | subgraph "Workbench Service"
9962 | DB[(SQL Database)]
9963 | FileStore[(File Storage)]
9964 | API[API Endpoints]
9965 | Events[Event System]
9966 | end
9967 |
9968 | subgraph "Assistant Instance"
9969 | AState[Assistant State API]
9970 | AStorage[Storage Directory]
9971 | EventHandler[Event Handler]
9972 | end
9973 |
9974 | subgraph "Conversation"
9975 | Messages[Messages]
9976 | Metadata[Message Metadata]
9977 | ConvFiles[Conversation Files]
9978 | end
9979 |
9980 | API <--> AState
9981 | API <--> Messages
9982 | API <--> ConvFiles
9983 | API <--> Metadata
9984 | FileStore <--> ConvFiles
9985 | DB <--> Messages
9986 | DB <--> Metadata
9987 | Events ---> EventHandler
9988 | EventHandler ---> AState
9989 | AState ---> AStorage
9990 |
9991 | classDef persistent fill:#b7e1cd,stroke:#82c3a6
9992 | classDef transient fill:#f8cecc,stroke:#b85450
9993 | classDef storage fill:#d5e8d4,stroke:#82b366
9994 |
9995 | class DB,FileStore,AStorage persistent
9996 | class Events transient
9997 | class ConvFiles,AState storage
9998 | ```
9999 |
10000 | ## State Storage Locations
10001 |
10002 | ```mermaid
10003 | graph LR
10004 | subgraph "Persistent Storage"
10005 | DB[(SQL Database<br>PostgreSQL/SQLite)]
10006 | FileStore[(File Storage<br>Binary Files)]
10007 | LocalFS[(Local Filesystem<br>JSON Files)]
10008 | end
10009 |
10010 | subgraph "State Types"
10011 | Messages[Messages]
10012 | MsgMeta[Message Metadata]
10013 | ConvFiles[Conversation Files]
10014 | AssistState[Assistant State API]
10015 | PrivateState[Private Assistant Data]
10016 | end
10017 |
10018 | Messages --> DB
10019 | MsgMeta --> DB
10020 | ConvFiles --> FileStore
10021 | AssistState --> DB
10022 | PrivateState --> LocalFS
10023 |
10024 | classDef db fill:#dae8fc,stroke:#6c8ebf
10025 | classDef file fill:#d5e8d4,stroke:#82b366
10026 | classDef local fill:#ffe6cc,stroke:#d79b00
10027 |
10028 | class DB db
10029 | class FileStore file
10030 | class LocalFS local
10031 | ```
10032 |
10033 | ## Conversation-level State Management
10034 |
10035 | ### Message Metadata
10036 |
10037 | - **Key Mechanism**: Each message can include arbitrary metadata as JSON
10038 | - **Storage**: Persisted in the database with the message
10039 | - **Special Fields**:
10040 | - `attribution`: Source information displayed after the sender
10041 | - `href`: Makes message a hyperlink
10042 | - `debug`: Debug information accessible through UI inspection
10043 | - `footer_items`: Additional information displayed in the message footer
10044 | - `tool_calls`: Structured representation of tool invocations
10045 | - `tool_result`: Results from tool executions
10046 | - **Usage**: Primary method for assistants to store structured data
10047 | - **Limitations**: Cannot be used to transfer large amounts of data (practical limit around 100KB)
10048 |
10049 | ### Files/Attachments
10050 |
10051 | - **Model**: Files are represented as `File` and `FileVersion` entities in the database
10052 | - **Storage**: Files are stored in a filesystem-based storage system managed by the Workbench service
10053 | - **Versioning**:
10054 | - Built-in version tracking for files with the same name
10055 | - All files with the same name in a conversation are treated as versions of the same file
10056 | - No way for users to explicitly designate a file with the same name as a new file rather than a new version
10057 | - **Access**:
10058 | - Files are visible in the conversation UI for users
10059 | - Files can be attached to messages via the `filenames` property
10060 | - Files can be uploaded, downloaded, and deleted through the UI
10061 | - **Permissions**: Files inherit conversation permissions
10062 | - **Limitations**:
10063 | - No direct UI for viewing or managing different versions of a file
10064 | - No way for users to specify storage at the assistant level versus conversation level
10065 |
10066 | ### Conversation Sharing
10067 |
10068 | - **Usage**: Enables controlled access to conversation content
10069 | - **Details**: See SHARING.md for comprehensive details on the sharing mechanism
10070 |
10071 | ### Events System
10072 |
10073 | - **Purpose**: Propagates changes to all conversation participants
10074 | - **Model**: `ConversationEvent` defines various event types
10075 | - **Types**: Message created/deleted, participant changes, etc.
10076 | - **Delivery**: Events delivered through Server-Sent Events (SSE)
10077 | - **Storage**: Events are not stored persistently - they are transient objects
10078 | - **Connection Requirements**: Recipients must be actively connected to receive events
10079 | - **Durability**: No event replay mechanism - events missed during disconnection are lost permanently
10080 | - **Limitations**: Cannot be relied upon for critical state synchronization due to potential message loss
10081 |
10082 | ```mermaid
10083 | sequenceDiagram
10084 | participant User as User
10085 | participant Assistant as Assistant
10086 | participant API as Workbench API
10087 | participant Events as Event System
10088 | participant DB as Database
10089 |
10090 | Note over User,DB: Both connected
10091 |
10092 | User->>API: Send message
10093 | API->>DB: Store message
10094 | API->>Events: Generate message_created event
10095 | Events->>User: Notify via SSE
10096 | Events->>Assistant: Notify via SSE
10097 |
10098 | Note over User,DB: Assistant disconnects
10099 |
10100 | User->>API: Edit message
10101 | API->>DB: Update message
10102 | API->>Events: Generate message_updated event
10103 | Events->>User: Notify via SSE
10104 | Events--xAssistant: Event lost (disconnected)
10105 |
10106 | Note over User,DB: Assistant reconnects
10107 |
10108 | Assistant->>API: Get conversation
10109 | API->>DB: Query messages
10110 | API->>Assistant: Return current state
10111 |
10112 | Note over User,DB: No automatic notification<br/>about missed events
10113 | ```
10114 |
10115 | ## Assistant-level State Management
10116 |
10117 | ### Assistant State API
10118 |
10119 | - **Type**: Server-side state managed by the Workbench service
10120 | - **Model**: Assistants maintain state via `StateResponseModel` in the database
10121 | - **Structure**: Structured data represented as JSON schema
10122 | - **UI Integration**: UI schema maintained for display formatting in Workbench UI
10123 | - **Description**: State descriptions provide user-facing information
10124 | - **Persistence**: State persisted in the central SQL database (PostgreSQL/SQLite)
10125 | - **Access**: Accessible via REST API endpoints in the Workbench service
10126 | - **Visibility**: Can be exposed to users through UI integrations
10127 |
10128 | ### Assistant Storage Directory
10129 |
10130 | - **Type**: Local filesystem storage specific to each assistant instance
10131 | - **Purpose**: Private storage for assistant-specific data
10132 | - **Location**: Typically `.data/assistants/[assistant-id]` directory
10133 | - **Implementation**: Created and managed by `storage.py` in semantic-workbench-assistant
10134 | - **Content**: Pydantic models serialized to JSON files via `write_model` function
10135 | - **Visibility**: Not exposed to users through the UI
10136 | - **Usage**: Appropriate for:
10137 | - Private cross-conversation data
10138 | - Assistant-specific configuration
10139 | - Cached or derived data
10140 | - Data that doesn't need UI visibility
10141 |
10142 | ### Configuration
10143 |
10144 | - **Storage**: Assistant configuration stored in metadata
10145 | - **Scope**: Configuration specific to each assistant instance
10146 | - **Access**: Available through assistant service APIs
10147 |
10148 | ## Cross-conversation Communication
10149 |
10150 | ### Cross-Conversation Access
10151 |
10152 | - Allows assistants to interact with multiple conversations simultaneously
10153 | - Requires proper permissions and active participant records
10154 | - Managed through the conversation sharing system
10155 | - See SHARING.md for comprehensive details on:
10156 | - Permission models and enforcement
10157 | - User vs. assistant access patterns
10158 | - Conversation sharing mechanisms
10159 | - Principal and participant relationships
10160 |
10161 | ### File Transfer
10162 |
10163 | - **Mechanism**: Files must be explicitly copied between conversations
10164 | - **Process**:
10165 | 1. Read file content from source conversation
10166 | 2. Create new file with same name in target conversation
10167 | 3. This creates a completely separate file, not linked to the original
10168 | - **Versioning**: Version history doesn't transfer between conversations
10169 |
10170 | ```mermaid
10171 | sequenceDiagram
10172 | participant ConvA as Conversation A
10173 | participant API as Workbench API
10174 | participant ConvB as Conversation B
10175 | participant FSA as File Storage A
10176 | participant FSB as File Storage B
10177 |
10178 | Note over ConvA,FSB: File Transfer Process
10179 |
10180 | ConvA->>API: Request file content<br/>(conversation_id_A, filename)
10181 | API->>FSA: Retrieve file content
10182 | FSA->>API: Return file content
10183 | API->>ConvA: File content
10184 |
10185 | ConvA->>API: Upload to conversation B<br/>(conversation_id_B, filename, content)
10186 | API->>FSB: Store file content
10187 | API->>ConvB: Create file record
10188 |
10189 | Note over ConvA,FSB: Result: Two independent files with<br/>same name but separate version history
10190 | ```
10191 |
10192 | ### Participant Model
10193 |
10194 | - **Types**: User participants and assistant participants
10195 | - **Status**: Online/offline status tracking
10196 | - **Permissions**: Read/write access controls
10197 | - **Events**: Participant join/leave events
10198 |
10199 | ## File Storage Architecture
10200 |
10201 | ### Storage System
10202 |
10203 | - **Physical Storage**: Files stored in filesystem managed by Workbench service
10204 | - **Reference System**: Messages and conversations reference files by name
10205 | - **Uniqueness**: Files are uniquely identified by the combination of conversation ID and filename
10206 | - **UI Integration**: Only conversation-level files are shown in the UI
10207 |
10208 | ```mermaid
10209 | graph TD
10210 | subgraph "File Management System"
10211 | FileDB[(File Database)]
10212 | FileStore[(Physical File Storage)]
10213 | end
10214 |
10215 | subgraph "Conversation 1"
10216 | Conv1[Conversation ID: 1]
10217 | File1A["File: report.pdf (v1)"]
10218 | File1B["File: report.pdf (v2)"]
10219 | Conv1 --- File1A
10220 | Conv1 --- File1B
10221 | end
10222 |
10223 | subgraph "Conversation 2"
10224 | Conv2[Conversation ID: 2]
10225 | File2["File: report.pdf (v1)"]
10226 | Conv2 --- File2
10227 | end
10228 |
10229 | subgraph "Message References"
10230 | Msg1["Message 1: with filenames=['report.pdf']"]
10231 | Msg2["Message 2: with filenames=['report.pdf']"]
10232 | end
10233 |
10234 | File1A -- "Version 1" --> FileStore
10235 | File1B -- "Version 2" --> FileStore
10236 | File2 -- "Version 1" --> FileStore
10237 |
10238 | File1A --> FileDB
10239 | File1B --> FileDB
10240 | File2 --> FileDB
10241 |
10242 | Msg1 -. "References latest version (v2)" .-> File1B
10243 | Msg2 -. "References latest version (v1)" .-> File2
10244 |
10245 | classDef conversation fill:#f9f,stroke:#333,stroke-width:2px
10246 | classDef file fill:#bbf,stroke:#333
10247 | classDef storage fill:#bfb,stroke:#333
10248 | classDef message fill:#fbb,stroke:#333
10249 |
10250 | class Conv1,Conv2 conversation
10251 | class File1A,File1B,File2 file
10252 | class FileDB,FileStore storage
10253 | class Msg1,Msg2 message
10254 | ```
10255 |
10256 | ### File Versioning
10257 |
10258 | - **Automatic Versioning**: When a file with the same name is uploaded to a conversation, it's treated as a new version
10259 | - **Version Control**: The system maintains version numbers and history
10260 | - **Access Control**: API allows requesting specific versions or defaulting to latest
10261 | - **Conflict Management**: No built-in conflict resolution for simultaneous updates
10262 |
10263 | ```mermaid
10264 | sequenceDiagram
10265 | participant User1 as User 1
10266 | participant Conv as Conversation
10267 | participant DB as Database
10268 | participant FS as File Storage
10269 |
10270 | User1->>Conv: Upload "report.pdf" (v1)
10271 | Conv->>DB: Create file record<br/>conversation_id: 123<br/>filename: "report.pdf"<br/>version: 1
10272 | Conv->>FS: Store file content with<br/>hash-based path
10273 |
10274 | Note over User1,FS: Later...
10275 |
10276 | User1->>Conv: Upload new "report.pdf"
10277 | Conv->>DB: Find existing file with<br/>same name in conversation
10278 | Conv->>DB: Create new version<br/>conversation_id: 123<br/>filename: "report.pdf"<br/>version: 2
10279 | Conv->>FS: Store new content with<br/>different hash-based path
10280 |
10281 | Note over User1,FS: Request file
10282 |
10283 | User1->>Conv: Request "report.pdf"
10284 | Conv->>DB: Find file, get latest version (v2)
10285 | Conv->>FS: Retrieve content for version 2
10286 | Conv->>User1: Return file content
10287 | ```
10288 |
10289 | ### File Naming
10290 |
10291 | - **Namespace**: Filenames must be unique within a conversation
10292 | - **Constraints**: Database enforces uniqueness via constraints
10293 | - **Workaround**: To have logically different files with the same name, users must use different filenames (e.g., "report-v2.pdf")
10294 |
10295 | ## Data Export/Import
10296 |
10297 | ### Serialization
10298 |
10299 | - Both conversations and assistants support export/import
10300 | - Data can be transferred between instances while maintaining relationships
10301 | - Export includes messages, files, and metadata
10302 |
10303 | ### Persistence
10304 |
10305 | - Database uses SQLModel with async SQLAlchemy
10306 | - Supports both SQLite and PostgreSQL databases
10307 |
10308 | ## Access Patterns
10309 |
10310 | ### Direct API Calls
10311 |
10312 | - Assistants can make authenticated API calls to the Workbench service
10313 | - API endpoints available for conversation, message, and file operations
10314 |
10315 | ### Message Commands
10316 |
10317 | - Assistants can respond to commands embedded in messages
10318 | - Command responses can include structured data in metadata
10319 |
10320 | ### Event Subscriptions
10321 |
10322 | - Assistants can subscribe to conversation events
10323 | - Real-time notifications of changes to conversations
10324 | - Requires maintaining active connections to receive events
10325 | - No guarantee of delivery - events during disconnections are lost
10326 | - Cannot be used as a reliable state synchronization mechanism
10327 |
10328 | ## Best Practices
10329 |
10330 | ### State Persistence
10331 |
10332 | - Use message metadata for small conversation-specific state (<100KB)
10333 | - Use assistant state for cross-conversation persistence that needs UI integration
10334 | - Use files for larger structured data or binary content that users should see
10335 | - Use assistant storage directory for private cross-conversation data
10336 |
10337 | ```mermaid
10338 | graph TD
10339 | State[State to Store]
10340 |
10341 | State -- "Small, conversation-specific<br/>(<100KB)" --> MessageMeta[Message Metadata]
10342 | State -- "Shared across conversations<br/>UI integration needed" --> AssistantState[Assistant State API]
10343 | State -- "Large data<br/>Binary content<br/>User visibility needed" --> Files[Conversation Files]
10344 | State -- "Cross-conversation<br/>Private to assistant<br/>No UI needed" --> LocalStorage[Assistant Storage Directory]
10345 |
10346 | MessageMeta --> Persist[(Database)]
10347 | AssistantState --> Persist
10348 | Files --> FileStore[(File Storage)]
10349 | LocalStorage --> Disk[(Local Filesystem)]
10350 |
10351 | classDef decision fill:#f5a9b8,stroke:#333
10352 | classDef storage fill:#dae8fc,stroke:#6c8ebf
10353 | classDef persist fill:#d5e8d4,stroke:#82b366
10354 |
10355 | class State decision
10356 | class MessageMeta,AssistantState,Files,LocalStorage storage
10357 | class Persist,FileStore,Disk persist
10358 | ```
10359 |
10360 | ### Sharing Information
10361 |
10362 | - Use conversation sharing for explicit permission grants
10363 | - Leverage the API for controlled cross-conversation access
10364 | - Use message metadata for lightweight information transfer
10365 | - For file sharing between conversations, implement explicit copy mechanisms
10366 |
10367 | ```mermaid
10368 | flowchart TD
10369 | Coordinator[Coordinator Conversation]
10370 | Team[Team Conversation]
10371 |
10372 | subgraph "Information Sharing Options"
10373 | direction TB
10374 | A[Message Metadata]
10375 | B[File Transfer]
10376 | C[Cross-conversation Messages]
10377 | D[Assistant State]
10378 | end
10379 |
10380 | Coordinator -- "Small data<br/>(<100KB)" --> A --> Team
10381 | Coordinator -- "Large/structured data<br/>User visibility" --> B --> Team
10382 | Coordinator -- "Real-time notifications" --> C --> Team
10383 | Coordinator -- "Persistent shared state" --> D --> Team
10384 |
10385 | classDef conv fill:#f9f,stroke:#333
10386 | classDef option fill:#bbf,stroke:#333
10387 |
10388 | class Coordinator,Team conv
10389 | class A,B,C,D option
10390 | ```
10391 |
10392 | ### Avoiding State Conflicts
10393 |
10394 | - Establish clear ownership of data to prevent conflicting updates
10395 | - Use versioning for tracking changes over time
10396 | - Implement conflict detection and resolution for collaborative scenarios
10397 | - Use timestamps to determine the most recent updates
10398 |
10399 | ```mermaid
10400 | sequenceDiagram
10401 | participant ConvA as Conversation A
10402 | participant Central as Central State
10403 | participant ConvB as Conversation B
10404 |
10405 | Note over ConvA,ConvB: State Synchronization Pattern
10406 |
10407 | ConvA->>Central: Read current state
10408 | ConvA->>ConvA: Local modifications
10409 | ConvA->>Central: Write with version check
10410 | Central-->>ConvA: Update confirmed (v1)
10411 |
10412 | ConvB->>Central: Read current state (v1)
10413 | ConvB->>ConvB: Local modifications
10414 |
10415 | ConvA->>Central: Read current state (v1)
10416 | ConvA->>ConvA: More local modifications
10417 | ConvA->>Central: Write with version check
10418 | Central-->>ConvA: Update confirmed (v2)
10419 |
10420 | ConvB->>Central: Write with version check (v1)
10421 | Central--xConvB: Conflict detected (current v2)
10422 | ConvB->>Central: Read current state (v2)
10423 | ConvB->>ConvB: Merge changes
10424 | ConvB->>Central: Write with version check (v2)
10425 | Central-->>ConvB: Update confirmed (v3)
10426 | ```
10427 |
10428 | === File: assistants/project-assistant/docs/notable_claude_conversations/clarifying_quad_modal_design.md ===
10429 | # Conversation about clarifying a design problem
10430 |
10431 | Note: This is an example of "backwards/reverse engineering" where the design/specs are updated based on code changes. I started out just trying to clarify a design that evolved, but ended up refreshing the entire codebase.
10432 |
10433 | Developer:
10434 |
10435 | ```markdown
10436 |
10437 | We probably shouldn't call the configuration template differences "modes". I can see you are already confusing this with our existing usage of the word "modes" to be whether or not the assistant is in "Coordinator mode" (in a conversation with the coordinator) or "Team mode" in a conversation with a team member. So, we have two configurations with two modes each (four different states). How would you suggest we modify the language to make communicate this simply?
10438 |
10439 | ```
10440 |
10441 | Claude:
10442 |
10443 | ```markdown
10444 | You're right about the terminology confusion. Let me clarify the distinction:
10445 |
10446 | 1. The assistant has two configuration templates:
10447 | - Default Project Assistant template
10448 | - Context Transfer template
10449 | 2. Each template supports two conversation roles:
10450 | - Coordinator role
10451 | - Team member role
10452 |
10453 | So we have a 2×2 matrix with four distinct states.
10454 |
10455 | I'd suggest using consistent terminology:
10456 | - "Configuration templates" for the two assistant types
10457 | - "Conversation roles" for coordinator vs team member
10458 | - Avoid using "mode" in either context to prevent confusion
10459 |
10460 | Would you like me to update the DESIGN.md with this clarified terminology?
10461 | ```
10462 |
10463 | Developer:
10464 |
10465 | ```markdown
10466 | Yes please. After we udpate the design doc, let's make a plan in the WORKING.md doc for how we can update all the code in the code base to support this design.
10467 | ```
10468 |
10469 | _... the assistant completed these and the developer then read through the new artifacts._
10470 |
10471 | Developer:
10472 |
10473 | ```markdown
10474 | We need to update the design quite a bit more as I notice it has lagged work we've done in the code base. Specifically, we changed the project "KB" to be the assistant "whiteboard", but I notice it hasn't been updated in the DESIGN.md doc. Let's update the design doc to reflect the actual code (look at it to make sure you understand the design concepts). When you are done, I'll tell you about two more things we need to update in the design doc.
10475 | ```
10476 |
10477 | _... assistant did work_
10478 |
10479 | Developer:
10480 |
10481 | ```markdown
10482 | You have correctly identified a few other aspects of the design doc we need to update. First of all, the stucture of the files in the "storage architecture" needs to be updated. At some point, we changed some of the directories and filename for the project data. Please take a look at the code (project_data.py is a good place to start) and update the design doc appropriately.
10483 | ```
10484 |
10485 | _... assistant did work_
10486 |
10487 | Developer:
10488 |
10489 | ```markdown
10490 | Excellent. We're almost there. Finally, at first we had the idea of a "dashboard" as being a data entity that would be represented in the state inspector, which is a visible tab in the Semantic Workbench. However, we noticed there was a lot of overlap between the dashboard and the other data entities and the state inspector started to contain other stuff that wasn't from the dashboard entity. This makes sense... we should have been coupling data with UI so directly. We made the decision to eliminate the dashboard entity in the code base and just build the state for the state inspector from other existing entities directly. To complete this decoupling, we needed to add a "project" entity (project.json) that held the overall info about the project (like the id and the share link, etc.). Please read the necessary code (at least state_inspector.py and project_data.py) and update the design doc to reflect our latest design.
10491 | ```
10492 |
10493 | _... assistant did work_
10494 |
10495 | And then we play the design changes forward back into the code base...
10496 |
10497 | Developer:
10498 |
10499 | ```markdown
10500 | Ok. This design looks up to date now. Based on your new knowledge, do you want to update anything in ow WORKING.md work plan?
10501 | ```
10502 |
10503 | The result here was pretty spectacular. It ended up finding many of the things I had "in the back of my head" that I knew I would need to update in the assistant eventually, and it even found several bugs that I didn't yet know about that were introduced from refactors. In essence, the process of collaboratively refreshing the design and then using the design to refresh the code base.
10504 |
10505 | WORKING.md:
10506 |
10507 | ```markdown
10508 | # Project Assistant Implementation Plan
10509 |
10510 | ## Configuration Templates Support
10511 |
10512 | This document outlines the implementation plan for maintaining and improving the dual configuration template architecture of the Project Assistant.
10513 |
10514 | ### Background
10515 |
10516 | The Project Assistant codebase supports two configuration templates:
10517 | 1. **Default Template** (full project management)
10518 | 2. **Context Transfer Template** (simplified knowledge sharing)
10519 |
10520 | Each template supports two conversation roles:
10521 | - **Coordinator Role**
10522 | - **Team Member Role**
10523 |
10524 | ### Current Implementation Status
10525 |
10526 | The basic architecture for configuration templates is already implemented:
10527 | - Configuration model classes defined in `assistant/configs/`
10528 | - Template selection in `assistant_config` in `chat.py`
10529 | - Different welcome messages for each template
10530 | - Support for disabling progress tracking in context transfer template
10531 | - UI adaptations in state inspector
10532 |
10533 | ### Improvement Plan
10534 |
10535 | #### 1. Configuration Template Refactoring
10536 |
10537 | 1. **Template Type Detection**
10538 | - Create a utility function to determine which template is active
10539 | - Use consistent naming: `is_context_transfer_template()`
10540 | - Add to a dedicated module for template utilities
10541 |
10542 | 2. **Configuration Model Improvements**
10543 | - Move any duplicate code between templates to base classes
10544 | - Create proper inheritance structure for template-specific configurations
10545 | - Add documentation for each configuration parameter
10546 | - Include validation rules to ensure template consistency
10547 |
10548 | #### 2. UI Adaptation Enhancements
10549 |
10550 | 1. **State Inspector Improvements**
10551 | - Ensure all labels consistently reflect the current template
10552 | - Fix duplicated context section in state inspector
10553 | - Enhance dynamic composition of data from multiple entities
10554 | - Optimize the presentation of project information without relying on a dedicated dashboard entity
10555 | - Add template-specific formatting for information requests
10556 | - Include template-specific explanatory text
10557 |
10558 | 2. **Welcome Messages**
10559 | - Review and update welcome messages for clarity
10560 | - Ensure context transfer template welcome message better explains its purpose
10561 | - Add contextual help tips for new users
10562 | - Provide template-specific onboarding guidance
10563 |
10564 | #### 3. Conversation Role Improvements
10565 |
10566 | 1. **Role Detection and Storage**
10567 | - Review role detection logic for robustness
10568 | - Ensure role information persists correctly
10569 | - Handle role conflicts gracefully
10570 |
10571 | 2. **Permission Management**
10572 | - Validate permissions for each role within each template
10573 | - Implement template-aware capability checks
10574 | - Ensure tool permissions match the current template
10575 |
10576 | #### 4. Tool Function Enhancements
10577 |
10578 | 1. **Template-Aware Tools**
10579 | - Update all tool functions to check the active template
10580 | - Disable progress tracking tools in context transfer template
10581 | - Add contextual success/error messages based on template
10582 |
10583 | 2. **LLM Prompting**
10584 | - Update system prompts to be template-aware
10585 | - Add template-specific examples to prompt text
10586 | - Ensure information request detection adapts to template context
10587 |
10588 | #### 5. Storage and Data Handling
10589 |
10590 | 1. **Model Adaptations**
10591 | - Ensure ProjectBrief model gracefully handles missing fields in context transfer
10592 | - Review all serialization/deserialization for template compatibility
10593 | - Add migration path for projects switching between templates
10594 | - Maintain clear separation between data entities and UI representation
10595 |
10596 | 2. **State Inspector Integration**
10597 | - Enhance state inspector to dynamically build UI from multiple data sources
10598 | - Ensure state inspector correctly adapts to the active template
10599 | - Optimize formatting of information for readability
10600 | - Add template-specific sections and labels to inspector view
10601 |
10602 | 3. **Whiteboard and Content Sharing**
10603 | - Improve automatic whiteboard updates with more intelligent content extraction
10604 | - Optimize coordinator conversation message sharing for better team context
10605 | - Implement content filtering to prioritize most relevant information
10606 |
10607 | #### 6. Documentation and Testing
10608 |
10609 | 1. **Documentation**
10610 | - Update all code comments to reflect template differences
10611 | - Document the 2×2 matrix of templates and roles
10612 | - Create template-specific usage examples
10613 | - Update design documentation to reflect data/UI decoupling
10614 | - Provide clear explanations of the ProjectInfo entity's role
10615 |
10616 | 2. **Testing**
10617 | - Add tests for template-specific behavior
10618 | - Test all combinations of templates and roles
10619 | - Verify state inspector correctly builds UI from multiple data sources
10620 | - Validate whiteboard auto-update functionality
10621 | - Automate validation of template consistency
10622 |
10623 | ### Implementation Sequence
10624 |
10625 | 1. First pass: Ensure all core functionality works in both templates
10626 | - Focus on critical paths and user-facing features
10627 | - Fix any current issues with template detection
10628 |
10629 | 2. Second pass: Improve template-specific user experience
10630 | - Refine UI elements and prompts
10631 | - Enhance tool behavior in each template
10632 |
10633 | 3. Third pass: Optimize and clean up
10634 | - Remove any redundant code
10635 | - Improve performance in both templates
10636 |
10637 | ### Success Criteria
10638 |
10639 | - Users can seamlessly switch between templates
10640 | - Both templates provide optimal experiences for their intended use cases
10641 | - Code remains maintainable with minimal template-specific logic
10642 | - Documentation clearly explains the differences between templates
10643 | ```
10644 |
10645 |
10646 | === File: assistants/project-assistant/docs/notable_claude_conversations/trying_the_context_agent.md ===
10647 |
10648 |
10649 | ```
10650 | reason: Ok, please craft me up a new message I can share out to others based upon the original recipe-executor_codebase-generator_cortex-platform_early-explorer-notes.md I attached. Make it a little easier to consume, while a) introducing them to the 3 projects (many have not heard of any of them, include links), b) capturing some of the more wild things shared (like the fact that the recipe-executor code is actually auto-generated from the docs/spec files via recipes and we iterate on the code through that, or that the entire cortex-core was built from a 1-page "plan" + vision doc, etc.) and c) also being very clear about the very early nature of this (we're literally mid-stream on all 3 at once, just enough to prove them out and now on to improving each one-by-one, making them more complete and accessible), and then include our share link.
10651 | ```
10652 |
10653 |
10654 | === File: assistants/project-assistant/project-assistant.code-workspace ===
10655 | {
10656 | "launch": {
10657 | "version": "0.2.0",
10658 | "configurations": [],
10659 | "compounds": [
10660 | {
10661 | "name": "semantic-workbench",
10662 | "configurations": [
10663 | "app: semantic-workbench-app",
10664 | "service: semantic-workbench-service"
10665 | ]
10666 | },
10667 | {
10668 | "name": "everything",
10669 | "configurations": [
10670 | "app: semantic-workbench-app",
10671 | "service: semantic-workbench-service",
10672 | "assistants: project-assistant"
10673 | ]
10674 | }
10675 | ]
10676 | },
10677 | // Naming convention: <root folder>:<package folder> (skip all subfolders)
10678 | "folders": [
10679 | {
10680 | "name": "docs",
10681 | "path": "../../docs"
10682 | },
10683 | {
10684 | "name": "workbench-app",
10685 | "path": "../../workbench-app"
10686 | },
10687 | {
10688 | "name": "workbench-service",
10689 | "path": "../../workbench-service"
10690 | },
10691 | {
10692 | "name": "assistants:codespace-assistant",
10693 | "path": "../../assistants/codespace-assistant"
10694 | },
10695 | // {
10696 | // "name": "assistants:skill-assistant",
10697 | // "path": "../../assistants/skill-assistant"
10698 | // },
10699 | {
10700 | "name": "assistants:project-assistant",
10701 | "path": "../../assistants/project-assistant"
10702 | },
10703 | {
10704 | "name": "libraries:assistant-drive",
10705 | "path": "../../libraries/python/assistant-drive"
10706 | },
10707 | {
10708 | "name": "libraries:assistant-extensions",
10709 | "path": "../../libraries/python/assistant-extensions"
10710 | },
10711 | {
10712 | "name": "libraries:events",
10713 | "path": "../../libraries/python/events"
10714 | },
10715 | {
10716 | "name": "libraries:llm-client",
10717 | "path": "../../libraries/python/llm-client"
10718 | },
10719 | {
10720 | "name": "libraries:openai-client",
10721 | "path": "../../libraries/python/openai-client"
10722 | },
10723 | {
10724 | "name": "libraries:semantic-workbench-api-model",
10725 | "path": "../../libraries/python/semantic-workbench-api-model"
10726 | },
10727 | {
10728 | "name": "libraries:semantic-workbench-assistant",
10729 | "path": "../../libraries/python/semantic-workbench-assistant"
10730 | },
10731 | {
10732 | "name": "tools",
10733 | "path": "../../tools"
10734 | },
10735 | {
10736 | "name": ".github",
10737 | "path": "../../.github"
10738 | },
10739 | {
10740 | "name": ".multi-root-tools",
10741 | "path": "../../.multi-root-tools"
10742 | }
10743 | ],
10744 | "extensions": {
10745 | "recommendations": [
10746 | "charliermarsh.ruff",
10747 | "dbaeumer.vscode-eslint",
10748 | "epivision.vscode-file-header",
10749 | "esbenp.prettier-vscode",
10750 | "ms-python.debugpy",
10751 | "ms-python.python",
10752 | "ms-vscode.makefile-tools",
10753 | "ms-vscode.vscode-node-azure-pack",
10754 | "tamasfe.even-better-toml"
10755 | ]
10756 | }
10757 | }
10758 |
10759 |
10760 | === File: assistants/project-assistant/pyproject.toml ===
10761 | [project]
10762 | name = "assistant"
10763 | version = "0.1.0"
10764 | description = "A file-sharing mediator assistant for collaborative projects."
10765 | authors = [{ name = "Semantic Workbench Team" }]
10766 | readme = "README.md"
10767 | requires-python = ">=3.11"
10768 | dependencies = [
10769 | "assistant-extensions[attachments]>=0.1.0",
10770 | "content-safety>=0.1.0",
10771 | "deepmerge>=2.0",
10772 | "openai>=1.61.0",
10773 | "openai-client>=0.1.0",
10774 | "semantic-workbench-assistant>=0.1.0",
10775 | "tiktoken>=0.8.0",
10776 | ]
10777 |
10778 | [dependency-groups]
10779 | dev = [
10780 | "pytest>=8.3.1",
10781 | "pytest-asyncio>=0.23.8",
10782 | "pytest-repeat>=0.9.3",
10783 | "pyright>=1.1.389",
10784 | ]
10785 |
10786 | [tool.uv]
10787 | package = true
10788 |
10789 | [tool.uv.sources]
10790 | assistant-extensions = { path = "../../libraries/python/assistant-extensions", editable = true }
10791 | content-safety = { path = "../../libraries/python/content-safety/", editable = true }
10792 | openai-client = { path = "../../libraries/python/openai-client", editable = true }
10793 | semantic-workbench-assistant = { path = "../../libraries/python/semantic-workbench-assistant", editable = true }
10794 |
10795 | [build-system]
10796 | requires = ["hatchling"]
10797 | build-backend = "hatchling.build"
10798 |
10799 | [tool.pyright]
10800 | exclude = ["**/.venv", "**/.data", "**/__pycache__"]
10801 |
10802 | [tool.pytest.ini_options]
10803 | addopts = "-vv"
10804 | log_cli = true
10805 | log_cli_level = "WARNING"
10806 | log_cli_format = "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s"
10807 | asyncio_mode = "auto"
10808 | asyncio_default_fixture_loop_scope = "function"
10809 |
10810 |
10811 | === File: assistants/project-assistant/test_inspector.py ===
10812 | """
10813 | Test script to verify state inspector functionality.
10814 | """
10815 |
10816 | import asyncio
10817 | import logging
10818 | from unittest.mock import AsyncMock, MagicMock
10819 |
10820 | from assistant.chat import assistant
10821 | from semantic_workbench_api_model.workbench_model import AssistantStateEvent
10822 | from semantic_workbench_assistant.assistant_app import ConversationContext
10823 |
10824 | # Set up logging
10825 | logging.basicConfig(level=logging.INFO)
10826 | logger = logging.getLogger(__name__)
10827 |
10828 |
10829 | async def test_inspector():
10830 | """Test the state inspector provider."""
10831 | # Create mock context
10832 | context = AsyncMock(spec=ConversationContext)
10833 | context.id = "test-conversation-id"
10834 | context.assistant = MagicMock()
10835 | context.assistant.id = "test-assistant-id"
10836 |
10837 | # Mock conversation
10838 | conversation = MagicMock()
10839 | conversation.metadata = {"setup_complete": True, "assistant_mode": "coordinator", "project_role": "coordinator"}
10840 | context.get_conversation.return_value = conversation
10841 |
10842 | # Create state event
10843 | state_event = AssistantStateEvent(state_id="project_status", event="focus", state=None)
10844 |
10845 | # Send event
10846 | logger.info("Sending state event...")
10847 | await context.send_conversation_state_event(state_event)
10848 |
10849 | # Get inspector provider
10850 | inspector_provider = assistant.inspector_state_providers.get("project_status")
10851 | if not inspector_provider:
10852 | logger.error("No project_status inspector provider found!")
10853 | return
10854 |
10855 | logger.info(f"Inspector provider found: {inspector_provider.display_name}")
10856 |
10857 | # Get state data
10858 | try:
10859 | state_data = await inspector_provider.get(context)
10860 | logger.info(f"State data: {state_data}")
10861 | except Exception as e:
10862 | logger.error(f"Error getting state data: {e}")
10863 |
10864 |
10865 | # Run the test
10866 | if __name__ == "__main__":
10867 | asyncio.run(test_inspector())
10868 |
10869 |
10870 | === File: assistants/project-assistant/tests/__init__.py ===
10871 | # Tests for project assistant
10872 |
10873 |
10874 | === File: assistants/project-assistant/tests/test_artifact_loading.py ===
10875 | """
10876 | Tests for the project storage functionality with the direct storage approach.
10877 | These tests replace the previous artifact-based tests.
10878 | """
10879 |
10880 | import pathlib
10881 | import shutil
10882 | import unittest
10883 | import unittest.mock
10884 | import uuid
10885 | from typing import Any, TypeVar
10886 |
10887 | from assistant.conversation_project_link import ConversationProjectManager
10888 | from assistant.project_data import Project, ProjectBrief, ProjectGoal, SuccessCriterion
10889 | from assistant.project_manager import ProjectManager
10890 | from assistant.project_storage import ProjectStorage, ProjectStorageManager
10891 | from assistant.project_storage_models import ConversationRole
10892 | from semantic_workbench_assistant import settings
10893 | from semantic_workbench_assistant.storage import read_model, write_model
10894 |
10895 | # Type variable for better type annotations
10896 | T = TypeVar("T")
10897 |
10898 |
10899 | class TestProjectStorage(unittest.IsolatedAsyncioTestCase):
10900 | """Test the project storage functionality with the new direct storage approach"""
10901 |
10902 | async def asyncSetUp(self):
10903 | # Create a test storage path
10904 | self.test_dir = pathlib.Path(__file__).parent.parent / ".data" / "test_project_storage"
10905 | self.test_dir.mkdir(exist_ok=True, parents=True)
10906 |
10907 | # Mock settings to use our test directory
10908 | self.original_storage_root = settings.storage.root
10909 | settings.storage.root = str(self.test_dir)
10910 |
10911 | # Create test project and conversation IDs
10912 | self.project_id = str(uuid.uuid4())
10913 | self.conversation_id = str(uuid.uuid4())
10914 | self.user_id = "test-user-id"
10915 | self.user_name = "Test User"
10916 |
10917 | # Create project directory structure
10918 | self.project_dir = ProjectStorageManager.get_project_dir(self.project_id)
10919 |
10920 | # Set up patching
10921 | self.patches = []
10922 |
10923 | # Create a mock context that will be recognized as ConversationContext
10924 | self.context = unittest.mock.MagicMock()
10925 | self.context.id = self.conversation_id
10926 |
10927 | # Mock assistant
10928 | mock_assistant = unittest.mock.MagicMock()
10929 | mock_assistant.id = "test-assistant-id"
10930 | self.context.assistant = mock_assistant
10931 |
10932 | # Patch storage_directory_for_context
10933 | def mock_storage_directory_for_context(context, *args, **kwargs):
10934 | return self.test_dir / f"context_{context.id}"
10935 |
10936 | patch1 = unittest.mock.patch(
10937 | "semantic_workbench_assistant.assistant_app.context.storage_directory_for_context",
10938 | side_effect=mock_storage_directory_for_context,
10939 | )
10940 | self.mock_storage_directory = patch1.start()
10941 | self.patches.append(patch1)
10942 |
10943 | # Patch get_associated_project_id
10944 | async def mock_get_associated_project_id(context):
10945 | return self.project_id
10946 |
10947 | patch2 = unittest.mock.patch.object(
10948 | ConversationProjectManager, "get_associated_project_id", side_effect=mock_get_associated_project_id
10949 | )
10950 | self.mock_get_project = patch2.start()
10951 | self.patches.append(patch2)
10952 |
10953 | # Patch get_conversation_role
10954 | async def mock_get_conversation_role(context):
10955 | return ConversationRole.COORDINATOR
10956 |
10957 | patch3 = unittest.mock.patch.object(
10958 | ConversationProjectManager, "get_conversation_role", side_effect=mock_get_conversation_role
10959 | )
10960 | self.mock_get_role = patch3.start()
10961 | self.patches.append(patch3)
10962 |
10963 | # Create a test brief
10964 | self.title = "Test Project"
10965 | self.create_test_brief()
10966 |
10967 | async def asyncTearDown(self):
10968 | # Clean up the test directory
10969 | if self.test_dir.exists():
10970 | shutil.rmtree(self.test_dir)
10971 |
10972 | # Restore settings
10973 | settings.storage.root = self.original_storage_root
10974 |
10975 | # Stop all patches
10976 | for patch in self.patches:
10977 | patch.stop()
10978 |
10979 | def create_test_brief(self):
10980 | """Create a test project brief in the project's shared directory"""
10981 | # Create a project brief
10982 | test_goal = ProjectGoal(
10983 | name="Test Goal",
10984 | description="This is a test goal",
10985 | success_criteria=[SuccessCriterion(description="Test criteria")],
10986 | )
10987 |
10988 | brief = ProjectBrief(
10989 | title=self.title,
10990 | description="Test project description",
10991 | created_by=self.user_id,
10992 | updated_by=self.user_id,
10993 | conversation_id=self.conversation_id,
10994 | )
10995 |
10996 | # Create a project with the goal
10997 | project = Project(
10998 | info=None,
10999 | brief=brief,
11000 | goals=[test_goal],
11001 | whiteboard=None,
11002 | )
11003 |
11004 | # Write the project to storage
11005 | project_path = ProjectStorageManager.get_project_path(self.project_id)
11006 | project_path.parent.mkdir(parents=True, exist_ok=True)
11007 | write_model(project_path, project)
11008 |
11009 | # Write to the project's shared directory using the correct path
11010 | brief_path = ProjectStorageManager.get_brief_path(self.project_id)
11011 | brief_path.parent.mkdir(parents=True, exist_ok=True)
11012 | write_model(brief_path, brief)
11013 |
11014 | async def test_get_project_brief(self) -> None:
11015 | """Test that get_project_brief correctly loads the brief from storage"""
11016 | # Mock the ProjectManager to use our test context
11017 | with unittest.mock.patch.object(ProjectManager, "get_project_id", return_value=self.project_id):
11018 | # Using Any here to satisfy type checker with our mock
11019 | context: Any = self.context
11020 |
11021 | # Get the brief using the ProjectManager
11022 | brief = await ProjectManager.get_project_brief(context)
11023 | project = ProjectStorage.read_project(self.project_id)
11024 |
11025 | # Verify the brief was loaded correctly
11026 | self.assertIsNotNone(brief, "Should load the brief")
11027 | if brief: # Type checking guard
11028 | self.assertEqual(brief.title, self.title)
11029 | self.assertEqual(brief.conversation_id, self.conversation_id)
11030 |
11031 | # Verify the project goals were loaded correctly
11032 | self.assertIsNotNone(project, "Should load the project")
11033 | if project: # Type checking guard
11034 | self.assertEqual(len(project.goals), 1, "Should have one goal")
11035 | self.assertEqual(project.goals[0].name, "Test Goal")
11036 |
11037 | async def test_direct_storage_access(self) -> None:
11038 | """Test direct access to project storage"""
11039 | # Test basic storage operations
11040 | brief_path = ProjectStorageManager.get_brief_path(self.project_id)
11041 |
11042 | # Read the brief directly using read_model
11043 | brief = read_model(brief_path, ProjectBrief)
11044 |
11045 | # Verify we got the correct brief
11046 | self.assertIsNotNone(brief, "Should load the brief directly")
11047 | if brief: # Type checking guard
11048 | self.assertEqual(brief.title, self.title)
11049 |
11050 | # Test updating the brief
11051 | brief.title = "Updated Project Title"
11052 | write_model(brief_path, brief)
11053 |
11054 | # Read it back to verify the update
11055 | updated_brief = read_model(brief_path, ProjectBrief)
11056 | if updated_brief: # Type checking guard
11057 | self.assertEqual(updated_brief.title, "Updated Project Title")
11058 |
11059 |
11060 | if __name__ == "__main__":
11061 | unittest.main()
11062 |
11063 |
11064 | === File: assistants/project-assistant/tests/test_project_manager.py ===
11065 | """
11066 | Tests for the ProjectManager functionality.
11067 | """
11068 |
11069 | from unittest.mock import AsyncMock, MagicMock
11070 |
11071 | import pytest
11072 | from assistant.project_manager import ProjectManager
11073 | from assistant.project_data import Project, ProjectInfo, ProjectGoal, SuccessCriterion, ProjectState
11074 | from semantic_workbench_assistant.assistant_app import ConversationContext
11075 |
11076 |
11077 | class TestProjectManager:
11078 | """Test the ProjectManager class."""
11079 |
11080 | @pytest.fixture
11081 | def context(self):
11082 | """Set up test fixtures."""
11083 | context = AsyncMock(spec=ConversationContext)
11084 | context.conversation = MagicMock()
11085 | context.id = "test-conversation-id"
11086 | return context
11087 |
11088 | @pytest.mark.asyncio
11089 | async def test_delete_project_goal(self, context, monkeypatch):
11090 | """Test the delete_project_goal method in ProjectManager."""
11091 | # Setup test data
11092 | project_id = "test-project-id"
11093 | goal_index = 1
11094 | goal_name = "Test Goal"
11095 | goal_description = "Test Description"
11096 |
11097 | # Create a test project with multiple goals
11098 | test_project = Project(
11099 | info=None,
11100 | brief=None,
11101 | goals=[
11102 | ProjectGoal(name="Goal 1", description="Description 1", priority=1, success_criteria=[]),
11103 | ProjectGoal(name=goal_name, description=goal_description, priority=2, success_criteria=[
11104 | SuccessCriterion(description="Criterion 1"),
11105 | SuccessCriterion(description="Criterion 2", completed=True),
11106 | ]),
11107 | ProjectGoal(name="Goal 3", description="Description 3", priority=3, success_criteria=[]),
11108 | ],
11109 | whiteboard=None,
11110 | requests=[],
11111 | )
11112 |
11113 | # Create test project info
11114 | test_project_info = ProjectInfo(
11115 | project_id=project_id,
11116 | coordinator_conversation_id="test-coordinator-id",
11117 | completed_criteria=1,
11118 | total_criteria=2,
11119 | progress_percentage=50,
11120 | version=1,
11121 | state=ProjectState.PLANNING
11122 | )
11123 |
11124 | # Mock get_project_id
11125 | async def mock_get_project_id(*args, **kwargs):
11126 | return project_id
11127 | monkeypatch.setattr("assistant.project_manager.ProjectManager.get_project_id",
11128 | AsyncMock(side_effect=mock_get_project_id))
11129 |
11130 | # Mock require_current_user
11131 | async def mock_require_current_user(*args, **kwargs):
11132 | return "test-user-id"
11133 | monkeypatch.setattr("assistant.project_manager.require_current_user",
11134 | AsyncMock(side_effect=mock_require_current_user))
11135 |
11136 | # Mock read_project
11137 | def mock_read_project(proj_id):
11138 | assert proj_id == project_id
11139 | return test_project
11140 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.read_project",
11141 | MagicMock(side_effect=mock_read_project))
11142 |
11143 | # Mock read_project_info
11144 | def mock_read_project_info(proj_id):
11145 | assert proj_id == project_id
11146 | return test_project_info
11147 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.read_project_info",
11148 | MagicMock(side_effect=mock_read_project_info))
11149 |
11150 | # Track if write_project and write_project_info were called with correct arguments
11151 | write_project_called = False
11152 | write_project_info_called = False
11153 |
11154 | # Mock write_project
11155 | def mock_write_project(proj_id, project):
11156 | nonlocal write_project_called
11157 | assert proj_id == project_id
11158 | # Verify goal was removed
11159 | assert len(project.goals) == 2
11160 | assert project.goals[0].name == "Goal 1"
11161 | assert project.goals[1].name == "Goal 3"
11162 | write_project_called = True
11163 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.write_project",
11164 | MagicMock(side_effect=mock_write_project))
11165 |
11166 | # Mock write_project_info
11167 | def mock_write_project_info(proj_id, project_info):
11168 | nonlocal write_project_info_called
11169 | assert proj_id == project_id
11170 | # Verify project info was updated
11171 | assert project_info.completed_criteria == 0 # Completed criterion was in the deleted goal
11172 | assert project_info.total_criteria == 0 # All criteria were in the deleted goal
11173 | assert project_info.progress_percentage == 0
11174 | assert project_info.version == 2 # Incremented
11175 | write_project_info_called = True
11176 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.write_project_info",
11177 | MagicMock(side_effect=mock_write_project_info))
11178 |
11179 | # Mock log_project_event
11180 | log_event_called = False
11181 | async def mock_log_project_event(*args, **kwargs):
11182 | nonlocal log_event_called
11183 | log_event_called = True
11184 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.log_project_event",
11185 | AsyncMock(side_effect=mock_log_project_event))
11186 |
11187 | # Mock notify_project_update
11188 | notify_called = False
11189 | async def mock_notify_project_update(*args, **kwargs):
11190 | nonlocal notify_called
11191 | notify_called = True
11192 | monkeypatch.setattr("assistant.project_manager.ProjectNotifier.notify_project_update",
11193 | AsyncMock(side_effect=mock_notify_project_update))
11194 |
11195 | # Mock refresh_all_project_uis
11196 | refresh_called = False
11197 | async def mock_refresh_all_project_uis(*args, **kwargs):
11198 | nonlocal refresh_called
11199 | refresh_called = True
11200 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.refresh_all_project_uis",
11201 | AsyncMock(side_effect=mock_refresh_all_project_uis))
11202 |
11203 | # Call the method being tested
11204 | success, goal_name_result = await ProjectManager.delete_project_goal(context, goal_index)
11205 |
11206 | # Verify the result
11207 | assert success is True
11208 | assert goal_name_result == goal_name
11209 |
11210 | # Verify all the expected actions were performed
11211 | assert write_project_called
11212 | assert write_project_info_called
11213 | assert log_event_called
11214 | assert notify_called
11215 | assert refresh_called
11216 |
11217 | @pytest.mark.asyncio
11218 | async def test_delete_project_goal_invalid_index(self, context, monkeypatch):
11219 | """Test deleting a goal with an invalid index."""
11220 | # Setup
11221 | project_id = "test-project-id"
11222 | goal_index = 5 # Out of range
11223 |
11224 | # Create a test project with fewer goals than the index
11225 | test_project = Project(
11226 | info=None,
11227 | brief=None,
11228 | goals=[
11229 | ProjectGoal(name="Goal 1", description="Description 1", priority=1, success_criteria=[]),
11230 | ProjectGoal(name="Goal 2", description="Description 2", priority=2, success_criteria=[]),
11231 | ],
11232 | whiteboard=None,
11233 | requests=[],
11234 | )
11235 |
11236 | # Mock get_project_id
11237 | async def mock_get_project_id(*args, **kwargs):
11238 | return project_id
11239 | monkeypatch.setattr("assistant.project_manager.ProjectManager.get_project_id",
11240 | AsyncMock(side_effect=mock_get_project_id))
11241 |
11242 | # Mock require_current_user
11243 | async def mock_require_current_user(*args, **kwargs):
11244 | return "test-user-id"
11245 | monkeypatch.setattr("assistant.project_manager.require_current_user",
11246 | AsyncMock(side_effect=mock_require_current_user))
11247 |
11248 | # Mock read_project
11249 | def mock_read_project(proj_id):
11250 | assert proj_id == project_id
11251 | return test_project
11252 | monkeypatch.setattr("assistant.project_manager.ProjectStorage.read_project",
11253 | MagicMock(side_effect=mock_read_project))
11254 |
11255 | # Call the method being tested with an invalid index
11256 | success, error_message = await ProjectManager.delete_project_goal(context, goal_index)
11257 |
11258 | # Verify the result indicates failure with appropriate error message
11259 | assert success is False
11260 | assert error_message is not None
11261 | assert "Invalid goal index" in str(error_message)
11262 |
11263 | @pytest.mark.asyncio
11264 | async def test_delete_project_goal_no_project(self, context, monkeypatch):
11265 | """Test deleting a goal when no project is associated with the conversation."""
11266 | # Mock get_project_id to return None
11267 | async def mock_get_project_id(*args, **kwargs):
11268 | return None
11269 | monkeypatch.setattr("assistant.project_manager.ProjectManager.get_project_id",
11270 | AsyncMock(side_effect=mock_get_project_id))
11271 |
11272 | # Call the method being tested
11273 | success, error_message = await ProjectManager.delete_project_goal(context, 1)
11274 |
11275 | # Verify the result indicates failure with appropriate error message
11276 | assert success is False
11277 | assert error_message is not None
11278 | assert "No project associated with this conversation" in str(error_message)
11279 |
11280 | === File: assistants/project-assistant/tests/test_project_storage.py ===
11281 | """
11282 | Tests for the direct project storage functionality.
11283 | """
11284 |
11285 | import pathlib
11286 | import shutil
11287 | import unittest
11288 | import unittest.mock
11289 | import uuid
11290 | from datetime import datetime
11291 |
11292 | from assistant.conversation_project_link import ConversationProjectManager
11293 | from assistant.project_data import (
11294 | InformationRequest,
11295 | LogEntry,
11296 | LogEntryType,
11297 | Project,
11298 | ProjectBrief,
11299 | ProjectGoal,
11300 | ProjectInfo,
11301 | ProjectLog,
11302 | ProjectWhiteboard,
11303 | RequestPriority,
11304 | RequestStatus,
11305 | SuccessCriterion,
11306 | )
11307 | from assistant.project_storage import ProjectStorage, ProjectStorageManager
11308 | from assistant.project_storage_models import (
11309 | ConversationRole,
11310 | CoordinatorConversationMessage,
11311 | CoordinatorConversationStorage,
11312 | )
11313 | from semantic_workbench_api_model.workbench_model import AssistantStateEvent
11314 | from semantic_workbench_assistant import settings
11315 | from semantic_workbench_assistant.storage import write_model
11316 |
11317 |
11318 | class TestProjectStorage(unittest.IsolatedAsyncioTestCase):
11319 | """Test the direct project storage functionality."""
11320 |
11321 | async def asyncSetUp(self):
11322 | """Set up test environment."""
11323 | # Create a test directory
11324 | self.test_dir = pathlib.Path(__file__).parent.parent / ".data" / "test_project_storage"
11325 | self.test_dir.mkdir(parents=True, exist_ok=True)
11326 |
11327 | # Mock settings to use our test directory
11328 | self.original_storage_root = settings.storage.root
11329 | settings.storage.root = str(self.test_dir)
11330 |
11331 | # Create test IDs
11332 | self.project_id = str(uuid.uuid4())
11333 | self.conversation_id = str(uuid.uuid4())
11334 | self.user_id = "test-user-id"
11335 |
11336 | # Create project directory structure
11337 | self.project_dir = ProjectStorageManager.get_project_dir(self.project_id)
11338 |
11339 | # Set up directories for different conversation roles
11340 | self.coordinator_dir = self.project_dir / ConversationRole.COORDINATOR.value
11341 | self.coordinator_dir.mkdir(exist_ok=True)
11342 |
11343 | self.team_dir = self.project_dir / f"team_{self.conversation_id}"
11344 | self.team_dir.mkdir(exist_ok=True)
11345 |
11346 | # Set up patching
11347 | self.patches = []
11348 |
11349 | # Create a mock context
11350 | self.context = unittest.mock.MagicMock()
11351 | self.context.id = self.conversation_id
11352 |
11353 | # Mock assistant
11354 | mock_assistant = unittest.mock.MagicMock()
11355 | mock_assistant.id = "test-assistant-id"
11356 | self.context.assistant = mock_assistant
11357 |
11358 | # Mock send_conversation_state_event
11359 | self.context.send_conversation_state_event = unittest.mock.AsyncMock()
11360 |
11361 | # Mock get_participants with the correct structure
11362 | participants_mock = unittest.mock.MagicMock()
11363 | participants_mock.participants = []
11364 | self.context.get_participants = unittest.mock.AsyncMock(return_value=participants_mock)
11365 |
11366 | # Patch storage_directory_for_context
11367 | def mock_storage_directory_for_context(context, *args, **kwargs):
11368 | return self.test_dir / f"context_{context.id}"
11369 |
11370 | patch1 = unittest.mock.patch(
11371 | "assistant.project_storage.storage_directory_for_context", side_effect=mock_storage_directory_for_context
11372 | )
11373 | self.mock_storage_directory = patch1.start()
11374 | self.patches.append(patch1)
11375 |
11376 | # Create initial test data
11377 | self.create_test_project_data()
11378 |
11379 | return None
11380 |
11381 | async def asyncTearDown(self):
11382 | """Clean up test environment."""
11383 | # Clean up the test directory
11384 | if self.test_dir.exists():
11385 | shutil.rmtree(self.test_dir)
11386 |
11387 | # Restore settings
11388 | settings.storage.root = self.original_storage_root
11389 |
11390 | # Stop all patches
11391 | for patch in self.patches:
11392 | patch.stop()
11393 |
11394 | def create_test_project_data(self):
11395 | """Create test project data."""
11396 | # Create a project brief
11397 | test_goal = ProjectGoal(
11398 | name="Test Goal",
11399 | description="This is a test goal",
11400 | success_criteria=[SuccessCriterion(description="Test criterion")],
11401 | )
11402 |
11403 | brief = ProjectBrief(
11404 | title="Test Project",
11405 | description="Test project description",
11406 | created_by=self.user_id,
11407 | updated_by=self.user_id,
11408 | conversation_id=self.conversation_id,
11409 | )
11410 |
11411 | # Create a Project with the goal
11412 | project = Project(
11413 | info=None,
11414 | brief=brief,
11415 | goals=[test_goal],
11416 | whiteboard=None,
11417 | )
11418 |
11419 | # Write the project to storage
11420 | project_path = ProjectStorageManager.get_project_path(self.project_id)
11421 | project_path.parent.mkdir(parents=True, exist_ok=True)
11422 | write_model(project_path, project)
11423 |
11424 | # Write brief to the proper path using ProjectStorage
11425 | brief_path = ProjectStorageManager.get_brief_path(self.project_id)
11426 | brief_path.parent.mkdir(parents=True, exist_ok=True)
11427 | write_model(brief_path, brief)
11428 |
11429 | # Create project info
11430 | project_info = ProjectInfo(
11431 | project_id=self.project_id,
11432 | coordinator_conversation_id=self.conversation_id,
11433 | )
11434 | project_info_path = ProjectStorageManager.get_project_info_path(self.project_id)
11435 | write_model(project_info_path, project_info)
11436 |
11437 | # Create an information request
11438 | request = InformationRequest(
11439 | request_id=str(uuid.uuid4()),
11440 | title="Test Request",
11441 | description="This is a test request",
11442 | priority=RequestPriority.HIGH,
11443 | status=RequestStatus.NEW, # Use enum value
11444 | created_by=self.user_id,
11445 | updated_by=self.user_id,
11446 | conversation_id=self.conversation_id,
11447 | )
11448 |
11449 | # Write request to the proper path using ProjectStorage
11450 | request_path = ProjectStorageManager.get_information_request_path(self.project_id, request.request_id)
11451 | request_path.parent.mkdir(parents=True, exist_ok=True)
11452 | write_model(request_path, request)
11453 |
11454 | # Create context directories
11455 | context_dir = self.test_dir / f"context_{self.conversation_id}"
11456 | context_dir.mkdir(exist_ok=True, parents=True)
11457 |
11458 | async def test_read_project_brief(self):
11459 | """Test reading a project brief."""
11460 | # Read the brief using ProjectStorage
11461 | brief = ProjectStorage.read_project_brief(self.project_id)
11462 | project = ProjectStorage.read_project(self.project_id)
11463 |
11464 | # Verify the brief was loaded correctly
11465 | self.assertIsNotNone(brief, "Should load the brief")
11466 | if brief: # Type checking guard
11467 | self.assertEqual(brief.title, "Test Project")
11468 | self.assertEqual(brief.description, "Test project description")
11469 |
11470 | # Verify the project was loaded with goals correctly
11471 | self.assertIsNotNone(project, "Should load the project")
11472 | if project: # Type checking guard
11473 | self.assertEqual(len(project.goals), 1)
11474 | self.assertEqual(project.goals[0].name, "Test Goal")
11475 |
11476 | async def test_read_information_request(self):
11477 | """Test reading an information request."""
11478 | # First get all requests to find the request ID
11479 | requests = ProjectStorage.get_all_information_requests(self.project_id)
11480 | self.assertEqual(len(requests), 1, "Should find one request")
11481 | request_id = requests[0].request_id
11482 |
11483 | # Read the request using ProjectStorage
11484 | request = ProjectStorage.read_information_request(self.project_id, request_id)
11485 |
11486 | # Verify the request was loaded correctly
11487 | self.assertIsNotNone(request, "Should load the request")
11488 | if request: # Type checking guard
11489 | self.assertEqual(request.title, "Test Request")
11490 | self.assertEqual(request.description, "This is a test request")
11491 | self.assertEqual(request.priority, RequestPriority.HIGH)
11492 |
11493 | async def test_write_project_log(self):
11494 | """Test writing a project log."""
11495 | # Create a log entry and proper LogEntry objects
11496 | log_entry = ProjectLog(
11497 | entries=[
11498 | LogEntry(
11499 | id=str(uuid.uuid4()),
11500 | timestamp=datetime.utcnow(),
11501 | entry_type=LogEntryType.INFORMATION_UPDATE,
11502 | message="Test log entry",
11503 | user_id=self.user_id,
11504 | user_name="Test User",
11505 | )
11506 | ],
11507 | )
11508 |
11509 | # Write the log
11510 | ProjectStorage.write_project_log(self.project_id, log_entry)
11511 |
11512 | # Read the log back
11513 | log = ProjectStorage.read_project_log(self.project_id)
11514 |
11515 | # Verify the log was saved and loaded correctly
11516 | self.assertIsNotNone(log, "Should load the log")
11517 | if log: # Type checking guard
11518 | self.assertEqual(len(log.entries), 1)
11519 | self.assertEqual(log.entries[0].entry_type, LogEntryType.INFORMATION_UPDATE)
11520 | self.assertEqual(log.entries[0].message, "Test log entry")
11521 |
11522 | async def test_project_directory_structure(self):
11523 | """Test the project directory structure."""
11524 | # Verify project directory exists
11525 | self.assertTrue(self.project_dir.exists(), "Project directory should exist")
11526 |
11527 | # Verify Coordinator directory exists
11528 | self.assertTrue(self.coordinator_dir.exists(), "Coordinator directory should exist")
11529 |
11530 | # Verify team directory exists
11531 | self.assertTrue(self.team_dir.exists(), "Team directory should exist")
11532 |
11533 | async def test_coordinator_conversation_storage(self):
11534 | """Test the coordinator conversation storage functionality."""
11535 | # Create coordinator conversation storage
11536 | messages = [
11537 | CoordinatorConversationMessage(
11538 | message_id=str(uuid.uuid4()),
11539 | content="Test message 1",
11540 | sender_name="Test User",
11541 | is_assistant=False,
11542 | ),
11543 | CoordinatorConversationMessage(
11544 | message_id=str(uuid.uuid4()),
11545 | content="Test message 2",
11546 | sender_name="Test Assistant",
11547 | is_assistant=True,
11548 | ),
11549 | ]
11550 |
11551 | conv_storage = CoordinatorConversationStorage(
11552 | project_id=self.project_id,
11553 | messages=messages,
11554 | )
11555 |
11556 | # Write to storage
11557 | ProjectStorage.write_coordinator_conversation(self.project_id, conv_storage)
11558 |
11559 | # Read back
11560 | read_storage = ProjectStorage.read_coordinator_conversation(self.project_id)
11561 |
11562 | # Verify data was saved correctly
11563 | self.assertIsNotNone(read_storage, "Should load the coordinator conversation")
11564 | if read_storage:
11565 | self.assertEqual(read_storage.project_id, self.project_id)
11566 | self.assertEqual(len(read_storage.messages), 2)
11567 | self.assertEqual(read_storage.messages[0].content, "Test message 1")
11568 | self.assertEqual(read_storage.messages[1].content, "Test message 2")
11569 | self.assertFalse(read_storage.messages[0].is_assistant)
11570 | self.assertTrue(read_storage.messages[1].is_assistant)
11571 |
11572 | async def test_append_coordinator_message(self):
11573 | """Test appending a message to coordinator conversation storage."""
11574 | # Start with empty storage
11575 | ProjectStorage.append_coordinator_message(
11576 | project_id=self.project_id,
11577 | message_id=str(uuid.uuid4()),
11578 | content="First message",
11579 | sender_name="Test User",
11580 | )
11581 |
11582 | # Append another message
11583 | ProjectStorage.append_coordinator_message(
11584 | project_id=self.project_id,
11585 | message_id=str(uuid.uuid4()),
11586 | content="Second message",
11587 | sender_name="Test Assistant",
11588 | is_assistant=True,
11589 | )
11590 |
11591 | # Read back
11592 | storage = ProjectStorage.read_coordinator_conversation(self.project_id)
11593 |
11594 | # Verify messages were added
11595 | self.assertIsNotNone(storage, "Should create and load the coordinator conversation")
11596 | if storage:
11597 | self.assertEqual(len(storage.messages), 2)
11598 | self.assertEqual(storage.messages[0].content, "First message")
11599 | self.assertEqual(storage.messages[1].content, "Second message")
11600 | self.assertFalse(storage.messages[0].is_assistant)
11601 | self.assertTrue(storage.messages[1].is_assistant)
11602 |
11603 | async def test_message_limit_in_coordinator_conversation(self):
11604 | """Test that coordinator conversation storage limits to the most recent messages."""
11605 | # Add more than 50 messages
11606 | for i in range(60):
11607 | ProjectStorage.append_coordinator_message(
11608 | project_id=self.project_id,
11609 | message_id=str(uuid.uuid4()),
11610 | content=f"Message {i + 1}",
11611 | sender_name="Test User",
11612 | )
11613 |
11614 | # Read back
11615 | storage = ProjectStorage.read_coordinator_conversation(self.project_id)
11616 |
11617 | # Verify only the most recent 50 messages are kept
11618 | self.assertIsNotNone(storage, "Should load the coordinator conversation")
11619 | if storage:
11620 | self.assertEqual(len(storage.messages), 50, "Should limit to 50 messages")
11621 | # First message should be the 11th message (since we keep the last 50 of 60)
11622 | self.assertEqual(storage.messages[0].content, "Message 11")
11623 | # Last message should be the 60th message
11624 | self.assertEqual(storage.messages[49].content, "Message 60")
11625 |
11626 | async def test_project_whiteboard(self):
11627 | """Test reading and writing project whiteboard."""
11628 | # Create whiteboard
11629 | whiteboard = ProjectWhiteboard(
11630 | content="# Test Whiteboard\n\nThis is a test whiteboard.",
11631 | is_auto_generated=True,
11632 | created_by=self.user_id,
11633 | updated_by=self.user_id,
11634 | conversation_id=self.conversation_id,
11635 | )
11636 |
11637 | # Write whiteboard
11638 | ProjectStorage.write_project_whiteboard(self.project_id, whiteboard)
11639 |
11640 | # Read whiteboard
11641 | read_whiteboard = ProjectStorage.read_project_whiteboard(self.project_id)
11642 |
11643 | # Verify whiteboard was saved correctly
11644 | self.assertIsNotNone(read_whiteboard, "Should load the whiteboard")
11645 | if read_whiteboard:
11646 | self.assertEqual(read_whiteboard.content, "# Test Whiteboard\n\nThis is a test whiteboard.")
11647 | self.assertTrue(read_whiteboard.is_auto_generated)
11648 |
11649 | async def test_refresh_current_ui(self):
11650 | """Test refreshing the current UI inspector."""
11651 | # Call refresh_current_ui
11652 | await ProjectStorage.refresh_current_ui(self.context)
11653 |
11654 | # Verify that send_conversation_state_event was called with correct parameters
11655 | self.context.send_conversation_state_event.assert_called_once()
11656 | called_event = self.context.send_conversation_state_event.call_args[0][0]
11657 | self.assertIsInstance(called_event, AssistantStateEvent)
11658 | self.assertEqual(called_event.state_id, "project_status")
11659 | self.assertEqual(called_event.event, "updated")
11660 | self.assertIsNone(called_event.state)
11661 |
11662 | async def test_project_info(self):
11663 | """Test reading and writing project info."""
11664 | # Read existing project info
11665 | project_info = ProjectStorage.read_project_info(self.project_id)
11666 |
11667 | # Verify it was loaded correctly
11668 | self.assertIsNotNone(project_info, "Should load project info")
11669 | if project_info:
11670 | self.assertEqual(project_info.project_id, self.project_id)
11671 | self.assertEqual(project_info.coordinator_conversation_id, self.conversation_id)
11672 |
11673 | # Update project info
11674 | if project_info:
11675 | project_info.status_message = "Test status message"
11676 | project_info.progress_percentage = 50
11677 | project_info.next_actions = ["Action 1", "Action 2"]
11678 |
11679 | # Write updated project info
11680 | ProjectStorage.write_project_info(self.project_id, project_info)
11681 |
11682 | # Read updated project info
11683 | updated_info = ProjectStorage.read_project_info(self.project_id)
11684 |
11685 | # Verify updates were saved
11686 | self.assertIsNotNone(updated_info, "Should load updated project info")
11687 | if updated_info:
11688 | self.assertEqual(updated_info.status_message, "Test status message")
11689 | self.assertEqual(updated_info.progress_percentage, 50)
11690 | self.assertEqual(updated_info.next_actions, ["Action 1", "Action 2"])
11691 |
11692 | async def test_get_linked_conversations_dir(self):
11693 | """Test getting linked conversations directory."""
11694 | # Get linked conversations directory
11695 | linked_dir = ProjectStorageManager.get_linked_conversations_dir(self.project_id)
11696 |
11697 | # Verify directory exists
11698 | self.assertTrue(linked_dir.exists(), "Linked conversations directory should exist")
11699 | self.assertEqual(linked_dir.name, "linked_conversations")
11700 |
11701 | async def test_conversation_association(self):
11702 | """Test conversation association with project."""
11703 | # Mock ConversationProjectManager.associate_conversation_with_project
11704 | with unittest.mock.patch("assistant.conversation_project_link.write_model") as mock_write_model:
11705 | # Mock conversation project path
11706 | conversation_project_file = ProjectStorageManager.get_conversation_project_file_path(self.context)
11707 |
11708 | # Call associate_conversation_with_project
11709 | await ConversationProjectManager.associate_conversation_with_project(self.context, self.project_id)
11710 |
11711 | # Verify write_model was called
11712 | mock_write_model.assert_called_once()
11713 |
11714 | # Verify the file path in the call
11715 | call_args = mock_write_model.call_args[0]
11716 | self.assertEqual(call_args[0], conversation_project_file)
11717 |
11718 | # Verify the ProjectAssociation object created
11719 | self.assertEqual(call_args[1].project_id, self.project_id)
11720 |
11721 | async def test_log_project_event(self):
11722 | """Test logging a project event."""
11723 |
11724 | # Create a test log entry directly
11725 | log_entry = LogEntry(
11726 | entry_type=LogEntryType.INFORMATION_UPDATE,
11727 | message="Test direct log entry",
11728 | user_id=self.user_id,
11729 | user_name="Test User",
11730 | related_entity_id="test-entity-id",
11731 | metadata={"test": "metadata"},
11732 | )
11733 |
11734 | # Create a log with the entry
11735 | log = ProjectLog(entries=[log_entry])
11736 |
11737 | # Write the log directly
11738 | ProjectStorage.write_project_log(self.project_id, log)
11739 |
11740 | # Read the log back
11741 | read_log = ProjectStorage.read_project_log(self.project_id)
11742 | self.assertIsNotNone(read_log, "Should load the log")
11743 | if read_log:
11744 | # Find our test entry
11745 | found_entry = False
11746 | for entry in read_log.entries:
11747 | if entry.message == "Test direct log entry":
11748 | found_entry = True
11749 | self.assertEqual(entry.entry_type, LogEntryType.INFORMATION_UPDATE)
11750 | self.assertEqual(entry.user_id, self.user_id)
11751 | self.assertEqual(entry.user_name, "Test User")
11752 | self.assertEqual(entry.related_entity_id, "test-entity-id")
11753 | self.assertEqual(entry.metadata, {"test": "metadata"})
11754 | self.assertTrue(found_entry, "Should find the added log entry")
11755 |
11756 |
11757 | if __name__ == "__main__":
11758 | unittest.main()
11759 |
11760 |
11761 | === File: assistants/project-assistant/tests/test_project_tools.py ===
11762 | """
11763 | Tests for the ProjectTools functionality.
11764 | """
11765 |
11766 | import contextlib
11767 | from unittest.mock import AsyncMock, MagicMock
11768 |
11769 | import openai_client
11770 | import pytest
11771 | from assistant.project_analysis import detect_information_request_needs
11772 | from assistant.project_storage_models import ConversationRole
11773 | from assistant.tools import ProjectTools
11774 | from semantic_workbench_assistant.assistant_app import ConversationContext
11775 |
11776 |
11777 | # Use pytest for all tests for consistency
11778 | class TestProjectTools:
11779 | """Test the ProjectTools class."""
11780 |
11781 | @pytest.fixture
11782 | def context(self):
11783 | """Set up test fixtures."""
11784 | context = AsyncMock(spec=ConversationContext)
11785 | context.conversation = MagicMock()
11786 | context.id = "test-conversation-id"
11787 | # Add the assistant attribute for the get_project_tools test
11788 | context.assistant = MagicMock()
11789 | # Use the correct property name (_template_id)
11790 | from assistant.utils import DEFAULT_TEMPLATE_ID
11791 | context.assistant._template_id = DEFAULT_TEMPLATE_ID
11792 | return context
11793 |
11794 | def test_initialization(self, context):
11795 | """Test that ProjectTools initializes correctly."""
11796 | # Test Coordinator role
11797 | coordinator_tools = ProjectTools(context, ConversationRole.COORDINATOR)
11798 | assert coordinator_tools.role == ConversationRole.COORDINATOR
11799 | assert coordinator_tools.tool_functions is not None
11800 |
11801 | # Verify Coordinator-specific functions are registered
11802 | assert "update_brief" in coordinator_tools.tool_functions.function_map
11803 | assert "add_project_goal" in coordinator_tools.tool_functions.function_map
11804 | assert "delete_project_goal" in coordinator_tools.tool_functions.function_map
11805 | assert "resolve_information_request" in coordinator_tools.tool_functions.function_map
11806 | assert "mark_project_ready_for_working" in coordinator_tools.tool_functions.function_map
11807 |
11808 | # Verify Team-specific functions are NOT registered
11809 | assert "create_information_request" not in coordinator_tools.tool_functions.function_map
11810 | assert "update_project_status" not in coordinator_tools.tool_functions.function_map
11811 | assert "mark_criterion_completed" not in coordinator_tools.tool_functions.function_map
11812 | assert "report_project_completion" not in coordinator_tools.tool_functions.function_map
11813 |
11814 | # Test Team role
11815 | team_tools = ProjectTools(context, ConversationRole.TEAM)
11816 | assert team_tools.role == ConversationRole.TEAM
11817 | assert team_tools.tool_functions is not None
11818 |
11819 | # Verify Team-specific functions are registered
11820 | assert "create_information_request" in team_tools.tool_functions.function_map
11821 | assert "update_project_status" in team_tools.tool_functions.function_map # Updated to match implementation
11822 | assert "mark_criterion_completed" in team_tools.tool_functions.function_map
11823 | assert "report_project_completion" in team_tools.tool_functions.function_map
11824 | assert "delete_information_request" in team_tools.tool_functions.function_map # Added new function
11825 |
11826 | # Verify Coordinator-specific functions are NOT registered
11827 | assert "create_project_brief" not in team_tools.tool_functions.function_map
11828 | assert "add_project_goal" not in team_tools.tool_functions.function_map
11829 | assert "resolve_information_request" not in team_tools.tool_functions.function_map
11830 | assert "mark_project_ready_for_working" not in team_tools.tool_functions.function_map
11831 |
11832 | # Verify common functions are registered for both roles
11833 | assert "suggest_next_action" in coordinator_tools.tool_functions.function_map
11834 |
11835 | # Verify team detection tool is not in Coordinator tools
11836 | assert "detect_information_request_needs" not in coordinator_tools.tool_functions.function_map
11837 |
11838 | assert "suggest_next_action" in team_tools.tool_functions.function_map
11839 |
11840 | # detect_information_request_needs is not exposed as a tool function anymore
11841 | assert "detect_information_request_needs" not in team_tools.tool_functions.function_map
11842 |
11843 | @pytest.mark.asyncio
11844 | async def test_project_tools_with_config(self, context, monkeypatch):
11845 | """Test the ProjectTools behavior with different configurations."""
11846 | # Mock the assistant_config.get method
11847 | mock_config = MagicMock()
11848 | mock_config.track_progress = True
11849 |
11850 | async def mock_get_config(*args, **kwargs):
11851 | return mock_config
11852 |
11853 | # Patch the assistant_config.get method
11854 | mock_assistant_config = MagicMock()
11855 | mock_assistant_config.get = AsyncMock(side_effect=mock_get_config)
11856 | monkeypatch.setattr("assistant.config.assistant_config", mock_assistant_config)
11857 |
11858 | # Test with track_progress set to True first
11859 | # Create a ProjectTools instance directly
11860 | tools = ProjectTools(context, ConversationRole.COORDINATOR)
11861 |
11862 | # Make sure add_project_goal was added when track_progress=True
11863 | assert "add_project_goal" in tools.tool_functions.function_map
11864 |
11865 | # For team role, check criterion completion
11866 | team_tools = ProjectTools(context, ConversationRole.TEAM)
11867 | assert "mark_criterion_completed" in team_tools.tool_functions.function_map
11868 |
11869 | # Now test with track_progress set to False
11870 | mock_config.track_progress = False
11871 |
11872 | # Test with get_project_tools which handles tool removal based on track_progress
11873 | # Since the track_progress check is now done in get_project_tools, we need to test that function
11874 |
11875 | # Create our own implementation to check for track_progress
11876 | async def check_tools_with_config(context, role):
11877 | """Simple wrapper to test if tools are filtered based on track_progress."""
11878 | tools = ProjectTools(context, role)
11879 |
11880 | # If progress tracking is disabled, remove progress-related tools
11881 | if not mock_config.track_progress:
11882 | # List of progress-related functions to remove
11883 | progress_functions = [
11884 | "add_project_goal",
11885 | "delete_project_goal",
11886 | "mark_criterion_completed",
11887 | "mark_project_ready_for_working",
11888 | "report_project_completion",
11889 | ]
11890 |
11891 | # Remove progress-related functions
11892 | for func_name in progress_functions:
11893 | if func_name in tools.tool_functions.function_map:
11894 | del tools.tool_functions.function_map[func_name]
11895 |
11896 | return tools
11897 |
11898 | # Get the tools using our function that checks track_progress
11899 | project_tools = await check_tools_with_config(context, ConversationRole.COORDINATOR)
11900 |
11901 | # Verify progress-tracking tools are removed when track_progress=False
11902 | assert "add_project_goal" not in project_tools.tool_functions.function_map
11903 | assert "mark_project_ready_for_working" not in project_tools.tool_functions.function_map
11904 |
11905 | # For team tools
11906 | team_tools = await check_tools_with_config(context, ConversationRole.TEAM)
11907 | assert "mark_criterion_completed" not in team_tools.tool_functions.function_map
11908 | assert "report_project_completion" not in team_tools.tool_functions.function_map
11909 |
11910 | @pytest.mark.asyncio
11911 | async def test_detect_information_request_needs(self, context, monkeypatch):
11912 | """Test the detect_information_request_needs function."""
11913 | # Create a more complete context mock for this test
11914 | context.assistant = MagicMock()
11915 | context.assistant._template_id = "default"
11916 | context.assistant.id = "test-assistant-id"
11917 |
11918 | # Test message
11919 | test_message = "I need information about how to proceed with this task."
11920 |
11921 | # Setup mock config to be returned from assistant_config.get
11922 | mock_config = MagicMock()
11923 | mock_config.track_progress = True
11924 | mock_config.service_config = None # Will cause the method to return early with error info
11925 |
11926 | async def mock_get_config(*args, **kwargs):
11927 | return mock_config
11928 |
11929 | # Patch assistant_config.get
11930 | mock_assistant_config = MagicMock()
11931 | mock_assistant_config.get = AsyncMock(side_effect=mock_get_config)
11932 | monkeypatch.setattr("assistant.project_analysis.assistant_config", mock_assistant_config)
11933 |
11934 | # Create a mock message for the message history
11935 | mock_msg = MagicMock()
11936 | mock_msg.sender = MagicMock()
11937 | mock_msg.sender.participant_id = "test-user-id" # Not the assistant ID
11938 | mock_msg.content = "Test message content"
11939 |
11940 | # Mock get_messages response
11941 | mock_messages_response = MagicMock()
11942 | mock_messages_response.messages = [mock_msg]
11943 | context.get_messages = AsyncMock(return_value=mock_messages_response)
11944 |
11945 | # Test with the message - should return early with missing service_config
11946 | result = await detect_information_request_needs(context, test_message)
11947 |
11948 | # Verify we get the expected early-return response for missing service_config
11949 | assert not result["is_information_request"]
11950 | assert "LLM detection unavailable" in result["reason"]
11951 | assert result["confidence"] == 0.0
11952 |
11953 | # Now update mock config with a service_config and simulate a successful LLM response
11954 | mock_config.service_config = {"type": "openai"}
11955 |
11956 | # Create mock client that returns expected response
11957 | mock_client = AsyncMock()
11958 | mock_response = MagicMock()
11959 | mock_choice = MagicMock()
11960 | mock_message = MagicMock()
11961 | mock_message.content = '{"is_information_request": true, "confidence": 0.9, "potential_title": "Test title"}'
11962 | mock_choice.message = mock_message
11963 | mock_response.choices = [mock_choice]
11964 | mock_client.chat.completions.create = AsyncMock(return_value=mock_response)
11965 |
11966 | # Mock the client creation context manager
11967 | @contextlib.asynccontextmanager
11968 | async def mock_create_client(*args, **kwargs):
11969 | try:
11970 | yield mock_client
11971 | finally:
11972 | pass
11973 |
11974 | # Patch the openai_client.create_client context manager
11975 | monkeypatch.setattr(openai_client, "create_client", mock_create_client)
11976 |
11977 | # Test with message that should return mocked success response
11978 | result = await detect_information_request_needs(context, test_message)
11979 |
11980 | # Verify successful path results
11981 | assert result["is_information_request"] is True
11982 | assert result["confidence"] == 0.9
11983 | assert result["potential_title"] == "Test title"
11984 | assert result["original_message"] == test_message
11985 |
11986 | @pytest.mark.asyncio
11987 | async def test_delete_project_goal(self, context, monkeypatch):
11988 | """Test the delete_project_goal functionality."""
11989 | # Create ProjectTools instance for Coordinator role
11990 | tools = ProjectTools(context, ConversationRole.COORDINATOR)
11991 |
11992 | # Setup mocks
11993 | project_id = "test-project-id"
11994 | goal_index = 1
11995 | goal_name = "Test Goal"
11996 |
11997 | # Mock ProjectManager.get_project_id
11998 | async def mock_get_project_id(*args, **kwargs):
11999 | return project_id
12000 | monkeypatch.setattr("assistant.project_manager.ProjectManager.get_project_id",
12001 | AsyncMock(side_effect=mock_get_project_id))
12002 |
12003 | # Mock require_current_user to return a user ID
12004 | async def mock_require_current_user(*args, **kwargs):
12005 | return "test-user-id"
12006 | monkeypatch.setattr("assistant.project_manager.require_current_user",
12007 | AsyncMock(side_effect=mock_require_current_user))
12008 |
12009 | # Mock ProjectManager.delete_project_goal to return success
12010 | async def mock_delete_project_goal(*args, **kwargs):
12011 | return True, goal_name
12012 | monkeypatch.setattr("assistant.project_manager.ProjectManager.delete_project_goal",
12013 | AsyncMock(side_effect=mock_delete_project_goal))
12014 |
12015 | # Test the delete_project_goal function
12016 | result = await tools.delete_project_goal(goal_index)
12017 |
12018 | # Verify the result
12019 | assert f"Goal '{goal_name}' has been successfully deleted from the project." in result
12020 |
12021 | # Verify that context.send_messages was called with appropriate message
12022 | expected_message_content = f"Goal '{goal_name}' has been successfully deleted from the project."
12023 | context.send_messages.assert_called_once()
12024 | # Get the first positional argument passed to send_messages
12025 | call_args = context.send_messages.call_args[0][0]
12026 | assert call_args.content == expected_message_content
12027 |
12028 | @pytest.mark.asyncio
12029 | async def test_delete_project_goal_wrong_role(self, context):
12030 | """Test delete_project_goal with wrong role (Team instead of Coordinator)."""
12031 | # Create ProjectTools instance for Team role
12032 | tools = ProjectTools(context, ConversationRole.TEAM)
12033 |
12034 | # Test the delete_project_goal function with Team role
12035 | result = await tools.delete_project_goal(1)
12036 |
12037 | # Verify that the operation is rejected
12038 | assert "Only Coordinator can delete project goals." in result
12039 | # Verify context.send_messages was not called
12040 | context.send_messages.assert_not_called()
12041 |
12042 | @pytest.mark.asyncio
12043 | async def test_delete_project_goal_error(self, context, monkeypatch):
12044 | """Test delete_project_goal with error condition."""
12045 | # Create ProjectTools instance for Coordinator role
12046 | tools = ProjectTools(context, ConversationRole.COORDINATOR)
12047 |
12048 | # Setup mocks
12049 | error_message = "Invalid goal index"
12050 |
12051 | # Mock ProjectManager.get_project_id
12052 | async def mock_get_project_id(*args, **kwargs):
12053 | return "test-project-id"
12054 | monkeypatch.setattr("assistant.project_manager.ProjectManager.get_project_id",
12055 | AsyncMock(side_effect=mock_get_project_id))
12056 |
12057 | # Mock ProjectManager.delete_project_goal to return failure
12058 | async def mock_delete_project_goal(*args, **kwargs):
12059 | return False, error_message
12060 | monkeypatch.setattr("assistant.project_manager.ProjectManager.delete_project_goal",
12061 | AsyncMock(side_effect=mock_delete_project_goal))
12062 |
12063 | # Test the delete_project_goal function
12064 | result = await tools.delete_project_goal(999) # Using an invalid index
12065 |
12066 | # Verify the error result
12067 | assert f"Error deleting goal: {error_message}" in result
12068 | # Verify context.send_messages was not called
12069 | context.send_messages.assert_not_called()
12070 |
12071 |
12072 | === File: assistants/project-assistant/tests/test_team_mode.py ===
12073 | """Tests for the Team conversation handler."""
12074 |
12075 | from datetime import datetime
12076 | from unittest.mock import AsyncMock, MagicMock
12077 |
12078 | import pytest
12079 |
12080 |
12081 | # Create mock classes for testing without importing from the actual modules
12082 | class ArtifactType:
12083 | PROJECT_BRIEF = "project_brief"
12084 | PROJECT_DASHBOARD = "project_dashboard"
12085 | INFORMATION_REQUEST = "information_request"
12086 | PROJECT_LOG = "project_log"
12087 | KNOWLEDGE_BASE = "project_kb"
12088 |
12089 |
12090 | class ProjectState:
12091 | PLANNING = "planning"
12092 | READY_FOR_WORKING = "ready_for_working"
12093 | IN_PROGRESS = "in_progress"
12094 | COMPLETED = "completed"
12095 | ABORTED = "aborted"
12096 |
12097 |
12098 | class RequestPriority:
12099 | LOW = "low"
12100 | MEDIUM = "medium"
12101 | HIGH = "high"
12102 | CRITICAL = "critical"
12103 |
12104 |
12105 | class RequestStatus:
12106 | NEW = "new"
12107 | ACKNOWLEDGED = "acknowledged"
12108 | IN_PROGRESS = "in_progress"
12109 | RESOLVED = "resolved"
12110 | DEFERRED = "deferred"
12111 | CANCELLED = "cancelled"
12112 |
12113 |
12114 | class LogEntryType:
12115 | REQUEST_CREATED = "request_created"
12116 | STATUS_CHANGED = "status_changed"
12117 | CRITERION_COMPLETED = "criterion_completed"
12118 | MILESTONE_PASSED = "milestone_passed"
12119 | PROJECT_COMPLETED = "project_completed"
12120 |
12121 |
12122 | class ProjectRole:
12123 | TEAM = "team"
12124 | COORDINATOR = "coordinator"
12125 |
12126 |
12127 | class MessageType:
12128 | chat = "chat"
12129 | notice = "notice"
12130 |
12131 |
12132 | class NewConversationMessage:
12133 | def __init__(self, content, message_type):
12134 | self.content = content
12135 | self.message_type = message_type
12136 |
12137 |
12138 | class BaseArtifact:
12139 | def __init__(
12140 | self,
12141 | artifact_id=None,
12142 | artifact_type=None,
12143 | created_by=None,
12144 | updated_by=None,
12145 | conversation_id=None,
12146 | project_id=None,
12147 | **kwargs,
12148 | ):
12149 | self.artifact_id = artifact_id or "test-artifact-id"
12150 | self.artifact_type = artifact_type
12151 | self.created_by = created_by
12152 | self.updated_by = updated_by
12153 | self.conversation_id = conversation_id
12154 | self.project_id = project_id
12155 | self.created_at = datetime.utcnow()
12156 | self.updated_at = datetime.utcnow()
12157 | self.version = 1
12158 | # Add any additional attributes
12159 | for key, value in kwargs.items():
12160 | setattr(self, key, value)
12161 |
12162 |
12163 | class InformationRequest(BaseArtifact):
12164 | def __init__(self, title=None, description=None, priority=None, status=None, **kwargs):
12165 | super().__init__(artifact_type=ArtifactType.INFORMATION_REQUEST, **kwargs)
12166 | self.title = title or "Test Request"
12167 | self.description = description or "Test Description"
12168 | self.priority = priority or RequestPriority.MEDIUM
12169 | self.status = status or RequestStatus.NEW
12170 | self.resolution = None
12171 | self.resolved_at = None
12172 | self.resolved_by = None
12173 |
12174 |
12175 | class ProjectDashboard(BaseArtifact):
12176 | def __init__(
12177 | self, state=None, progress_percentage=0, active_requests=None, completed_criteria=0, total_criteria=0, **kwargs
12178 | ):
12179 | super().__init__(artifact_type=ArtifactType.PROJECT_DASHBOARD, **kwargs)
12180 | self.state = state or ProjectState.PLANNING
12181 | self.progress_percentage = progress_percentage
12182 | self.active_requests = active_requests or []
12183 | self.completed_criteria = completed_criteria
12184 | self.total_criteria = total_criteria
12185 | self.status_message = None
12186 |
12187 |
12188 | class SuccessCriterion:
12189 | def __init__(self, id=None, description=None, completed=False, completed_by=None):
12190 | self.id = id or "test-criterion-id"
12191 | self.description = description or "Test criterion"
12192 | self.completed = completed
12193 | self.completed_at = None if not completed else datetime.utcnow()
12194 | self.completed_by = completed_by
12195 |
12196 |
12197 | class ProjectGoal:
12198 | def __init__(self, id=None, name=None, description=None, priority=1, success_criteria=None):
12199 | self.id = id or "test-goal-id"
12200 | self.name = name or "Test Goal"
12201 | self.description = description or "Test Goal Description"
12202 | self.priority = priority
12203 | self.success_criteria = success_criteria or []
12204 |
12205 |
12206 | class ProjectBrief(BaseArtifact):
12207 | def __init__(self, title=None, description=None, goals=None, **kwargs):
12208 | super().__init__(artifact_type=ArtifactType.PROJECT_BRIEF, **kwargs)
12209 | self.title = title or "Test Project"
12210 | self.description = description or "Test Description"
12211 | self.goals = goals or []
12212 |
12213 |
12214 | # Create a mock for the TeamConversationHandler
12215 | class MockTeamConversationHandler:
12216 | def __init__(self, context):
12217 | self.context = context
12218 | self.log_action = AsyncMock()
12219 |
12220 | async def create_information_request(self, title, description, priority=RequestPriority.MEDIUM):
12221 | # Mock implementation
12222 | request = InformationRequest(
12223 | title=title,
12224 | description=description,
12225 | priority=priority,
12226 | created_by="test-user-id",
12227 | updated_by="test-user-id",
12228 | conversation_id=str(self.context.id),
12229 | project_id="test-project-id",
12230 | )
12231 |
12232 | # Call mocked log_action
12233 | await self.log_action(
12234 | LogEntryType.REQUEST_CREATED,
12235 | f"Created information request: {title}",
12236 | related_artifact_id=request.artifact_id,
12237 | related_artifact_type=ArtifactType.INFORMATION_REQUEST,
12238 | )
12239 |
12240 | # Send notification
12241 | await self.context.send_messages(
12242 | NewConversationMessage(
12243 | content=f"Created information request: {title}",
12244 | message_type=MessageType.notice,
12245 | )
12246 | )
12247 |
12248 | return True, f"Created information request: {title}", request
12249 |
12250 | async def update_project_info(self, progress_percentage, status_message=None):
12251 | # Mock implementation
12252 | dashboard = ProjectDashboard(
12253 | state=ProjectState.IN_PROGRESS,
12254 | progress_percentage=progress_percentage,
12255 | created_by="test-user-id",
12256 | updated_by="test-user-id",
12257 | conversation_id=str(self.context.id),
12258 | project_id="test-project-id",
12259 | )
12260 | dashboard.status_message = status_message
12261 |
12262 | # Call mocked log_action for state change and progress update
12263 | await self.log_action(LogEntryType.MILESTONE_PASSED, "Project is now in progress")
12264 |
12265 | await self.log_action(LogEntryType.STATUS_CHANGED, f"Updated project progress to {progress_percentage}%")
12266 |
12267 | # Send notification
12268 | await self.context.send_messages(
12269 | NewConversationMessage(
12270 | content=f"Updated project progress to {progress_percentage}%",
12271 | message_type=MessageType.notice,
12272 | )
12273 | )
12274 |
12275 | return True, f"Updated project progress to {progress_percentage}%", dashboard
12276 |
12277 | async def mark_criterion_completed(self, goal_id, criterion_id):
12278 | # Mock implementation
12279 | criterion = SuccessCriterion(id=criterion_id, description="Test criterion")
12280 | criterion.completed = True
12281 | criterion.completed_at = datetime.utcnow()
12282 | criterion.completed_by = "test-user-id"
12283 |
12284 | dashboard = ProjectDashboard(
12285 | state=ProjectState.IN_PROGRESS,
12286 | progress_percentage=100,
12287 | completed_criteria=1,
12288 | total_criteria=1,
12289 | created_by="test-user-id",
12290 | updated_by="test-user-id",
12291 | conversation_id=str(self.context.id),
12292 | project_id="test-project-id",
12293 | )
12294 |
12295 | # Call mocked log_action
12296 | await self.log_action(LogEntryType.CRITERION_COMPLETED, f"Completed criterion: {criterion.description}")
12297 |
12298 | # Send notification
12299 | await self.context.send_messages(
12300 | NewConversationMessage(
12301 | content=f"Marked criterion '{criterion.description}' as completed.",
12302 | message_type=MessageType.notice,
12303 | )
12304 | )
12305 |
12306 | return True, f"Marked criterion '{criterion.description}' as completed.", dashboard
12307 |
12308 | async def report_project_completion(self, completion_summary):
12309 | # Mock implementation
12310 | dashboard = ProjectDashboard(
12311 | state=ProjectState.COMPLETED,
12312 | progress_percentage=100,
12313 | created_by="test-user-id",
12314 | updated_by="test-user-id",
12315 | conversation_id=str(self.context.id),
12316 | project_id="test-project-id",
12317 | )
12318 | dashboard.status_message = completion_summary
12319 |
12320 | # Call mocked log_action
12321 | await self.log_action(LogEntryType.PROJECT_COMPLETED, "Project marked as completed")
12322 |
12323 | # Send notification
12324 | await self.context.send_messages(
12325 | NewConversationMessage(
12326 | content="🎉 Project has been marked as completed.",
12327 | message_type=MessageType.notice,
12328 | )
12329 | )
12330 |
12331 | return True, "Project has been marked as completed", dashboard
12332 |
12333 | async def get_project_info(self):
12334 | # Mock implementation
12335 | return {
12336 | "has_project": True,
12337 | "project_id": "test-project-id",
12338 | "role": "team",
12339 | "title": "Test Project",
12340 | "description": "A test project",
12341 | "status": "in_progress",
12342 | "progress": 50,
12343 | "open_requests": 0,
12344 | "pending_requests": [],
12345 | }
12346 |
12347 |
12348 | class TestTeamConversationHandler:
12349 | """Test cases for TeamConversationHandler."""
12350 |
12351 | @pytest.fixture
12352 | def mock_context(self):
12353 | """Create a mock conversation context."""
12354 | context = AsyncMock()
12355 | context.id = "test-conversation-id"
12356 | context.assistant = MagicMock()
12357 | context.assistant.id = "test-assistant-id"
12358 | context.get_participants = AsyncMock()
12359 |
12360 | participants = MagicMock()
12361 | participant = MagicMock()
12362 | participant.id = "test-user-id"
12363 | participant.name = "Test User"
12364 | participant.role = "user"
12365 | participants.participants = [participant]
12366 |
12367 | context.get_participants.return_value = participants
12368 | context.send_messages = AsyncMock()
12369 |
12370 | return context
12371 |
12372 | @pytest.fixture
12373 | def team_handler(self, mock_context):
12374 | """Create a MockTeamConversationHandler instance with a mock context."""
12375 | return MockTeamConversationHandler(mock_context)
12376 |
12377 | @pytest.mark.asyncio
12378 | async def test_create_information_request(self, team_handler, mock_context):
12379 | """Test creating an information request."""
12380 | # Call the method
12381 | success, message, request = await team_handler.create_information_request(
12382 | "Test Request", "This is a test request", RequestPriority.HIGH
12383 | )
12384 |
12385 | # Assertions
12386 | assert success is True
12387 | assert "Created information request: Test Request" in message
12388 | assert request is not None
12389 | assert request.title == "Test Request"
12390 | assert request.description == "This is a test request"
12391 | assert request.priority == RequestPriority.HIGH
12392 | assert request.created_by == "test-user-id"
12393 |
12394 | # Verify that a notification was sent
12395 | mock_context.send_messages.assert_called_once()
12396 |
12397 | # Verify log_action was called
12398 | team_handler.log_action.assert_called_once()
12399 |
12400 | @pytest.mark.asyncio
12401 | async def test_update_project_info(self, team_handler, mock_context):
12402 | """Test updating the project information."""
12403 | # Call the method
12404 | success, message, dashboard = await team_handler.update_project_info(50, "Making progress in the team")
12405 |
12406 | # Assertions
12407 | assert success is True
12408 | assert "Updated project progress to 50%" in message
12409 | assert dashboard is not None
12410 | assert dashboard.progress_percentage == 50
12411 | assert dashboard.status_message == "Making progress in the team"
12412 | assert dashboard.state == ProjectState.IN_PROGRESS
12413 |
12414 | # Verify that a notification was sent
12415 | mock_context.send_messages.assert_called_once()
12416 |
12417 | # Verify log_action was called twice (once for state change, once for progress update)
12418 | assert team_handler.log_action.call_count == 2
12419 |
12420 | @pytest.mark.asyncio
12421 | async def test_mark_criterion_completed(self, team_handler, mock_context):
12422 | """Test marking a success criterion as completed."""
12423 | # Call the method
12424 | success, message, updated_dashboard = await team_handler.mark_criterion_completed(
12425 | "test-goal-id", "test-criterion-id"
12426 | )
12427 |
12428 | # Assertions
12429 | assert success is True
12430 | assert "Marked criterion" in message
12431 | assert updated_dashboard is not None
12432 | assert updated_dashboard.completed_criteria == 1
12433 | assert updated_dashboard.total_criteria == 1
12434 | assert updated_dashboard.progress_percentage == 100 # 1/1 = 100%
12435 |
12436 | # Verify that a notification was sent
12437 | mock_context.send_messages.assert_called_once()
12438 |
12439 | # Verify log_action was called
12440 | team_handler.log_action.assert_called_once()
12441 |
12442 | @pytest.mark.asyncio
12443 | async def test_report_project_completion(self, team_handler, mock_context):
12444 | """Test reporting project completion."""
12445 | # Call the method
12446 | success, message, dashboard = await team_handler.report_project_completion(
12447 | "Project has been successfully completed with all objectives achieved."
12448 | )
12449 |
12450 | # Assertions
12451 | assert success is True
12452 | assert "Project has been marked as completed" in message
12453 | assert dashboard is not None
12454 | assert dashboard.state == ProjectState.COMPLETED
12455 | assert dashboard.progress_percentage == 100
12456 | assert dashboard.status_message == "Project has been successfully completed with all objectives achieved."
12457 |
12458 | # Verify that a notification was sent
12459 | mock_context.send_messages.assert_called_once()
12460 |
12461 | # Verify log_action was called
12462 | team_handler.log_action.assert_called_once()
12463 |
12464 | @pytest.mark.asyncio
12465 | async def test_get_project_info(self, team_handler, mock_context):
12466 | """Test getting project info."""
12467 | # Call the method
12468 | project_info = await team_handler.get_project_info()
12469 |
12470 | # Assertions
12471 | assert project_info["has_project"] is True
12472 | assert project_info["project_id"] == "test-project-id"
12473 | assert project_info["role"] == "team"
12474 | assert project_info["title"] == "Test Project"
12475 | assert project_info["status"] == "in_progress"
12476 | assert project_info["progress"] == 50
12477 |
12478 |
12479 |
```