marvin 3.2.0__tar.gz → 3.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {marvin-3.2.0 → marvin-3.2.2}/PKG-INFO +1 -1
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-run.mdx +2 -0
- marvin-3.2.2/examples/slackbot/src/slackbot/_internal/thread_status.py +94 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/api.py +81 -3
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/core.py +3 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/slack.py +3 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/actor.py +11 -1
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/orchestrator.py +16 -1
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/run.py +32 -7
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/task.py +5 -1
- marvin-3.2.2/tests/ai/tasks/test_tasks.py +34 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/tasks/test_tasks.py +7 -0
- marvin-3.2.2/tests/test_concurrent_execution.py +191 -0
- marvin-3.2.0/tests/ai/tasks/test_tasks.py +0 -14
- {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/docs.mdc +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/general.mdc +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/python.mdc +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/labeler.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/release.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/deploy-cloudrun-revision-community.yaml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/deploy-cloudrun-revision-internal.yaml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/image-build-and-push-community.yaml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/image-build-and-push-internal.yaml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/labeler.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/publish.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/run-tests.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/static-analysis.yml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.gitignore +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/.pre-commit-config.yaml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/LICENSE +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/alembic.ini +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-actor.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-agent.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-names.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-team.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-beta.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-config.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-dev.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-main.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-migrations.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-database.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-defaults.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-end_turn.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-events.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-llm.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-orchestrator.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-streaming.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-cast.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-classify.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-extract.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-fn.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-generate.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-plan.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-say.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-summarize.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-handlers.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-print_handler.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-queue_handler.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-instructions.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-memory.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-chroma.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-lance.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-postgres.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-qdrant.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-prompts.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-settings.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tasks-task.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tasks.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-thread.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tools-thread.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tools.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-asyncio.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-jinja.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-jsonschema.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-logging.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-models.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-testing.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-tools.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-types.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/brand/marvin_mascot.jpeg +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/code.css +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/ai_model.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/dont_panic.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/it_hates_me.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/life_the_universe_and_ai.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/pip_install_marvin_hero.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/sort_of_thing_you_enjoy.png +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/community.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/agents.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/memory.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/tasks.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/teams.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/threads.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/tools-and-context.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/docs.json +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/cast.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/classify.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/extract.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/fn.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/generate.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/plan.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/run.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/say.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/functions/summarize.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/building-a-chatbot.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/cli.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/configuration.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/configure-llms.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/database-migrations.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/mcp.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/migration-guide.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/guides/multi-step-workflow.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/installation.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/instructions.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/interactivity.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/memory.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/running-tasks.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/task-results.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/tools.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/quickstart.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/snippets/version-badge.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/style.css +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/docs/welcome.mdx +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/agent_mcp.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/custom_handler.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/custom_prompt.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/deepseek_chat.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/english_to_schema.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/fastmcp/hello.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_cast.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_classify.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_extract.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_fn.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_generate.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_marvin.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_run.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_task.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/hello_team.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/pandas/data.csv +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/pandas/dataframe.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/provider_specific/azure/run_agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/provider_specific/openai/run_agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/.dockerignore +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/Dockerfile.slackbot +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/pyproject.toml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/__main__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/constants.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/monkey_patch.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/templates.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/tool_tracking.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/assets.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/client.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/discussions.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/exceptions.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/issues.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/models.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/prompts.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/py.typed +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/research_agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/search.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/settings.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/strings.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/types.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/wrap.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/talk_to_marvin.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/task_retries.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/timeseries.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/you_have_been_goosed/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/examples/you_have_been_goosed/goose_em.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/justfile +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/env.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/script.py.mako +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250301_141903_initial_schema.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250302_123716_improve_message_tracking.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250302_135101_add_llmcall_message_tracking.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/pyproject.toml +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/scripts/check_for_breakpoints.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/scripts/create_mintlify_api_ref.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/deprecation.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/fastmcp.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/mcp.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/names.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/team.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/beta/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/config.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/dev.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/main.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/migrations.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/database.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/defaults.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/end_turn.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/events.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/llm.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/streaming.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/cast.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/classify.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/extract.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/fn.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/generate.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/plan.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/say.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/summarize.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/handlers.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/print_handler.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/queue_handler.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/instructions.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/instructions.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/memory.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/chroma.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/lance.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/postgres.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/qdrant.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/prompts.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/py.typed +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/settings.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/agent.jinja +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/memory.jinja +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/system.jinja +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/task.jinja +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/team.jinja +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/thread.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/CLAUDE.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/interactive/cli.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/thread.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/asyncio.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/jinja.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/jsonschema.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/logging.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/models.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/testing.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/tools.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/types.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/agents/test_mcp_integration.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_cast.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_classify.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_customization.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_extract.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_fn.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_generate.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_run.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_say.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_summarize.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/memory/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/memory/test_memory.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/tasks/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/ai/test_mcp.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/README.md +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/test_agent.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/test_team.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/conftest.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_database.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_events.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_orchestrator.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_streaming.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_threads.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/memory/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/memory/test_memory.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/tasks/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/test_instructions.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/test_run.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/__init__.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_jsonschema.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_tools.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_types.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/conftest.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/data/youre-funny.wav +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/tests/settings/test_settings_object.py +0 -0
- {marvin-3.2.0 → marvin-3.2.2}/uv.lock +0 -0
|
@@ -32,11 +32,13 @@ def run_stream(instructions: str | Sequence[UserContent], result_type: type[T] =
|
|
|
32
32
|
```python
|
|
33
33
|
def run_tasks(tasks: list[Task[Any]], thread: Thread | str | None = None, raise_on_failure: bool = True, handlers: list[Handler | AsyncHandler] | None = None) -> list[Task[Any]]
|
|
34
34
|
```
|
|
35
|
+
Run tasks either concurrently (if independent) or sequentially.
|
|
35
36
|
|
|
36
37
|
### `run_tasks_async`
|
|
37
38
|
```python
|
|
38
39
|
def run_tasks_async(tasks: list[Task[Any]], thread: Thread | str | None = None, raise_on_failure: bool = True, handlers: list[Handler | AsyncHandler] | None = None) -> list[Task[Any]] | AsyncGenerator[Event, None]
|
|
39
40
|
```
|
|
41
|
+
Run tasks either concurrently (if independent) or sequentially via orchestrator.
|
|
40
42
|
|
|
41
43
|
### `run_tasks_stream`
|
|
42
44
|
```python
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Thread status tracking for Slack events (cross-process safe).
|
|
2
|
+
|
|
3
|
+
This module encapsulates a tiny SQLite-backed status mechanism to avoid duplicate
|
|
4
|
+
processing of the same Slack thread when users edit their original post or when
|
|
5
|
+
Slack delivers multiple events. It keeps the rest of the app simple and avoids
|
|
6
|
+
sprawling status logic across files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from slackbot.core import Database
|
|
14
|
+
|
|
15
|
+
Status = Literal["in_progress", "completed"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def ensure_schema(db: Database) -> None:
|
|
19
|
+
"""Ensure the status table exists.
|
|
20
|
+
|
|
21
|
+
Using CREATE TABLE IF NOT EXISTS is idempotent and cheap enough to call
|
|
22
|
+
whenever we touch the status table, avoiding module-level state or locks.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def _create() -> None:
|
|
26
|
+
db.con.execute(
|
|
27
|
+
"""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS slack_thread_status (
|
|
29
|
+
thread_ts TEXT PRIMARY KEY,
|
|
30
|
+
status TEXT NOT NULL CHECK (status IN ('in_progress','completed')),
|
|
31
|
+
updated_at REAL NOT NULL
|
|
32
|
+
);
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
db.con.commit()
|
|
36
|
+
|
|
37
|
+
await db.loop.run_in_executor(db.executor, _create)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def try_acquire(db: Database, thread_ts: str) -> bool:
|
|
41
|
+
"""Attempt to mark a thread as in_progress; returns True if acquired.
|
|
42
|
+
|
|
43
|
+
Uses an atomic INSERT OR IGNORE on a PRIMARY KEY to prevent duplicates across
|
|
44
|
+
concurrent processes or tasks.
|
|
45
|
+
"""
|
|
46
|
+
await ensure_schema(db)
|
|
47
|
+
|
|
48
|
+
def _insert() -> int:
|
|
49
|
+
cur = db.con.cursor()
|
|
50
|
+
cur.execute(
|
|
51
|
+
"""
|
|
52
|
+
INSERT OR IGNORE INTO slack_thread_status (thread_ts, status, updated_at)
|
|
53
|
+
VALUES (?, 'in_progress', strftime('%s','now'))
|
|
54
|
+
""",
|
|
55
|
+
(thread_ts,),
|
|
56
|
+
)
|
|
57
|
+
db.con.commit()
|
|
58
|
+
return cur.rowcount
|
|
59
|
+
|
|
60
|
+
rowcount: int = await db.loop.run_in_executor(db.executor, _insert)
|
|
61
|
+
return rowcount == 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_status(db: Database, thread_ts: str) -> Status | None:
|
|
65
|
+
await ensure_schema(db)
|
|
66
|
+
|
|
67
|
+
def _query() -> Status | None:
|
|
68
|
+
cur = db.con.cursor()
|
|
69
|
+
cur.execute(
|
|
70
|
+
"SELECT status FROM slack_thread_status WHERE thread_ts = ?",
|
|
71
|
+
(thread_ts,),
|
|
72
|
+
)
|
|
73
|
+
row = cur.fetchone()
|
|
74
|
+
return row[0] if row else None # type: ignore[return-value]
|
|
75
|
+
|
|
76
|
+
return await db.loop.run_in_executor(db.executor, _query)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def mark_completed(db: Database, thread_ts: str) -> None:
|
|
80
|
+
await ensure_schema(db)
|
|
81
|
+
|
|
82
|
+
def _update() -> None:
|
|
83
|
+
cur = db.con.cursor()
|
|
84
|
+
cur.execute(
|
|
85
|
+
"""
|
|
86
|
+
UPDATE slack_thread_status
|
|
87
|
+
SET status = 'completed', updated_at = strftime('%s','now')
|
|
88
|
+
WHERE thread_ts = ?
|
|
89
|
+
""",
|
|
90
|
+
(thread_ts,),
|
|
91
|
+
)
|
|
92
|
+
db.con.commit()
|
|
93
|
+
|
|
94
|
+
await db.loop.run_in_executor(db.executor, _update)
|
|
@@ -17,6 +17,15 @@ from pydantic_ai.messages import ModelMessage
|
|
|
17
17
|
|
|
18
18
|
from slackbot._internal.constants import WORKSPACE_TO_CHANNEL_ID
|
|
19
19
|
from slackbot._internal.templates import CHANNEL_REDIRECT_MESSAGE, WELCOME_MESSAGE
|
|
20
|
+
from slackbot._internal.thread_status import (
|
|
21
|
+
get_status as get_thread_status,
|
|
22
|
+
)
|
|
23
|
+
from slackbot._internal.thread_status import (
|
|
24
|
+
mark_completed as mark_thread_completed,
|
|
25
|
+
)
|
|
26
|
+
from slackbot._internal.thread_status import (
|
|
27
|
+
try_acquire as try_acquire_thread,
|
|
28
|
+
)
|
|
20
29
|
from slackbot.assets import summarize_thread
|
|
21
30
|
from slackbot.core import (
|
|
22
31
|
Database,
|
|
@@ -40,6 +49,9 @@ BOT_MENTION = r"<@(\w+)>"
|
|
|
40
49
|
|
|
41
50
|
logger = get_logger(__name__)
|
|
42
51
|
|
|
52
|
+
# Duplicate handling is coordinated via a small SQLite table in
|
|
53
|
+
# _internal.thread_status to work across processes.
|
|
54
|
+
|
|
43
55
|
|
|
44
56
|
def get_designated_channel_for_workspace(team_id: str) -> str | None:
|
|
45
57
|
"""Get the designated channel ID for a given workspace team ID."""
|
|
@@ -106,6 +118,33 @@ async def run_agent(
|
|
|
106
118
|
raise
|
|
107
119
|
|
|
108
120
|
|
|
121
|
+
def _extract_message_context(event: Any) -> tuple[bool, str | None, str | None, str]:
|
|
122
|
+
"""Return (is_edit, message_ts, thread_ts, text) for Slack events.
|
|
123
|
+
|
|
124
|
+
- For `message_changed` events, Slack nests the edited message under `event.message`.
|
|
125
|
+
- For normal app_mention events, fields are at the top level.
|
|
126
|
+
"""
|
|
127
|
+
is_edit = getattr(event, "subtype", None) == "message_changed"
|
|
128
|
+
msg = (getattr(event, "message", None) or {}) if is_edit else {}
|
|
129
|
+
|
|
130
|
+
# Prefer the message ts for idempotency; fall back to event_ts if needed
|
|
131
|
+
message_ts = (
|
|
132
|
+
msg.get("ts")
|
|
133
|
+
if is_edit
|
|
134
|
+
else (getattr(event, "ts", None) or getattr(event, "event_ts", None))
|
|
135
|
+
)
|
|
136
|
+
# Thread anchor where we should post replies
|
|
137
|
+
thread_ts = (
|
|
138
|
+
(msg.get("thread_ts") or msg.get("ts"))
|
|
139
|
+
if is_edit
|
|
140
|
+
else (getattr(event, "thread_ts", None) or getattr(event, "ts", None))
|
|
141
|
+
)
|
|
142
|
+
# Text used for bot mention detection
|
|
143
|
+
text = (msg.get("text") if is_edit else (getattr(event, "text", None) or "")) or ""
|
|
144
|
+
|
|
145
|
+
return is_edit, message_ts, thread_ts, text
|
|
146
|
+
|
|
147
|
+
|
|
109
148
|
@flow(name="Handle Slack Message", retries=1)
|
|
110
149
|
async def handle_message(payload: SlackPayload, db: Database):
|
|
111
150
|
logger = get_run_logger()
|
|
@@ -115,9 +154,10 @@ async def handle_message(payload: SlackPayload, db: Database):
|
|
|
115
154
|
return Completed(message="Invalid event", name="SKIPPED")
|
|
116
155
|
|
|
117
156
|
USER_MESSAGE_MAX_TOKENS = settings.user_message_max_tokens
|
|
118
|
-
|
|
119
|
-
|
|
157
|
+
# Determine message context accommodating edit events
|
|
158
|
+
is_edit, message_ts, thread_ts, user_message = _extract_message_context(event)
|
|
120
159
|
assert thread_ts is not None, "No thread_ts found"
|
|
160
|
+
assert message_ts is not None, "No message_ts found"
|
|
121
161
|
cleaned_message = re.sub(BOT_MENTION, "", user_message).strip()
|
|
122
162
|
msg_len = count_tokens(cleaned_message)
|
|
123
163
|
|
|
@@ -137,6 +177,34 @@ async def handle_message(payload: SlackPayload, db: Database):
|
|
|
137
177
|
return Completed(message="Message too long", name="SKIPPED")
|
|
138
178
|
|
|
139
179
|
if re.search(BOT_MENTION, user_message) and payload.authorizations:
|
|
180
|
+
# Per-message acquire: prevent duplicate handling for this specific message
|
|
181
|
+
acquired = await try_acquire_thread(db, message_ts)
|
|
182
|
+
if not acquired:
|
|
183
|
+
status = await get_thread_status(db, message_ts)
|
|
184
|
+
if status == "in_progress" and is_edit:
|
|
185
|
+
assert event.channel is not None, (
|
|
186
|
+
"Event channel is None when posting edit-ignored notice"
|
|
187
|
+
)
|
|
188
|
+
await post_slack_message(
|
|
189
|
+
message=(
|
|
190
|
+
"✋ I noticed you edited your original message. "
|
|
191
|
+
"I'm already working on your first version — please add any "
|
|
192
|
+
"clarifications as new messages in this thread so I don't lose track."
|
|
193
|
+
),
|
|
194
|
+
channel_id=event.channel,
|
|
195
|
+
thread_ts=thread_ts,
|
|
196
|
+
)
|
|
197
|
+
return Completed(
|
|
198
|
+
message="Ignored edit while in progress",
|
|
199
|
+
name="IGNORED_EDIT",
|
|
200
|
+
data=dict(message_ts=message_ts, thread_ts=thread_ts),
|
|
201
|
+
)
|
|
202
|
+
return Completed(
|
|
203
|
+
message="Duplicate event for message",
|
|
204
|
+
name="SKIPPED_DUPLICATE",
|
|
205
|
+
data=dict(message_ts=message_ts, thread_ts=thread_ts),
|
|
206
|
+
)
|
|
207
|
+
|
|
140
208
|
# Check if this is the designated channel
|
|
141
209
|
team_id = payload.team_id or ""
|
|
142
210
|
is_designated = check_if_designated_channel(event.channel, team_id)
|
|
@@ -188,7 +256,11 @@ async def handle_message(payload: SlackPayload, db: Database):
|
|
|
188
256
|
|
|
189
257
|
try:
|
|
190
258
|
result = await run_agent(
|
|
191
|
-
cleaned_message,
|
|
259
|
+
cleaned_message,
|
|
260
|
+
conversation,
|
|
261
|
+
user_context,
|
|
262
|
+
event.channel,
|
|
263
|
+
thread_ts,
|
|
192
264
|
) # type: ignore
|
|
193
265
|
|
|
194
266
|
await db.add_thread_messages(thread_ts, result.new_messages())
|
|
@@ -213,6 +285,12 @@ async def handle_message(payload: SlackPayload, db: Database):
|
|
|
213
285
|
name="ERROR_HANDLED",
|
|
214
286
|
data=dict(error=str(e), user_context=user_context),
|
|
215
287
|
)
|
|
288
|
+
finally:
|
|
289
|
+
try:
|
|
290
|
+
await mark_thread_completed(db, message_ts)
|
|
291
|
+
except Exception:
|
|
292
|
+
logger.warning("Failed to mark message as completed")
|
|
293
|
+
|
|
216
294
|
return Completed(
|
|
217
295
|
message="Responded to mention",
|
|
218
296
|
data=dict(user_context=user_context, conversation=conversation),
|
|
@@ -129,6 +129,9 @@ class Database:
|
|
|
129
129
|
|
|
130
130
|
await self.loop.run_in_executor(self.executor, _insert)
|
|
131
131
|
|
|
132
|
+
# Note: Thread status tracking is handled in-process within api.py to keep
|
|
133
|
+
# persistence minimal for the examples package.
|
|
134
|
+
|
|
132
135
|
|
|
133
136
|
@task(task_run_name="build user context for {user_id}")
|
|
134
137
|
def build_user_context(
|
|
@@ -29,7 +29,10 @@ class EventBlock(BaseModel):
|
|
|
29
29
|
class SlackEvent(BaseModel):
|
|
30
30
|
client_msg_id: str | None = None
|
|
31
31
|
type: str
|
|
32
|
+
subtype: str | None = None
|
|
32
33
|
text: str | None = None
|
|
34
|
+
# For message_changed edit events, Slack nests the edited message here
|
|
35
|
+
message: dict[str, Any] | None = None
|
|
33
36
|
user: str | dict[str, Any] | None = None
|
|
34
37
|
ts: str | None = None
|
|
35
38
|
team: str | None = None
|
|
@@ -76,7 +76,17 @@ class Actor(ABC):
|
|
|
76
76
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any):
|
|
77
77
|
"""Reset the current actor in context."""
|
|
78
78
|
if self._tokens: # Only reset if we have tokens
|
|
79
|
-
|
|
79
|
+
try:
|
|
80
|
+
_current_actor.reset(self._tokens.pop())
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
# Token was created in a different async context (e.g., asyncio.gather)
|
|
83
|
+
# This happens when tasks run concurrently and is expected behavior
|
|
84
|
+
if "was created in a different Context" in str(e):
|
|
85
|
+
# This is the expected concurrent execution case - ignore safely
|
|
86
|
+
pass
|
|
87
|
+
else:
|
|
88
|
+
# Some other ValueError - re-raise it
|
|
89
|
+
raise
|
|
80
90
|
|
|
81
91
|
@classmethod
|
|
82
92
|
def get_current(cls) -> "Actor | None":
|
|
@@ -143,7 +143,22 @@ class Orchestrator:
|
|
|
143
143
|
if actor is None:
|
|
144
144
|
actor = tasks[0].get_actor()
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
# Get tasks assigned to this actor
|
|
147
|
+
potential_tasks = [t for t in tasks if actor is t.get_actor()]
|
|
148
|
+
|
|
149
|
+
# For independent tasks, only assign one per turn to avoid EndTurn conflicts
|
|
150
|
+
if len(potential_tasks) > 1:
|
|
151
|
+
# Check if any tasks depend on each other
|
|
152
|
+
has_deps = any(
|
|
153
|
+
t2 in t1.depends_on or t1 in t2.depends_on
|
|
154
|
+
for t1 in potential_tasks
|
|
155
|
+
for t2 in potential_tasks
|
|
156
|
+
if t1 != t2
|
|
157
|
+
)
|
|
158
|
+
# If independent, process one at a time
|
|
159
|
+
assigned_tasks = [potential_tasks[0]] if not has_deps else potential_tasks
|
|
160
|
+
else:
|
|
161
|
+
assigned_tasks = potential_tasks
|
|
147
162
|
|
|
148
163
|
# Mark tasks as running if they're pending
|
|
149
164
|
for task in assigned_tasks:
|
|
@@ -15,19 +15,43 @@ from marvin.handlers.handlers import AsyncHandler, Handler
|
|
|
15
15
|
T = TypeVar("T")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _tasks_are_independent(tasks: list[Task[Any]]) -> bool:
|
|
19
|
+
"""Check if tasks have no dependencies between each other."""
|
|
20
|
+
for i, task1 in enumerate(tasks):
|
|
21
|
+
for j, task2 in enumerate(tasks):
|
|
22
|
+
if i != j:
|
|
23
|
+
# Check if task1 depends on task2 or vice versa
|
|
24
|
+
if task2 in task1.depends_on or task1 in task2.depends_on:
|
|
25
|
+
return False
|
|
26
|
+
# Check if they share subtasks or parent relationships
|
|
27
|
+
if task1.parent == task2 or task2.parent == task1:
|
|
28
|
+
return False
|
|
29
|
+
if task1 in task2.subtasks or task2 in task1.subtasks:
|
|
30
|
+
return False
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
18
34
|
async def run_tasks_async(
|
|
19
35
|
tasks: list[Task[Any]],
|
|
20
36
|
thread: Thread | str | None = None,
|
|
21
37
|
raise_on_failure: bool = True,
|
|
22
38
|
handlers: list[Handler | AsyncHandler] | None = None,
|
|
23
39
|
) -> list[Task[Any]] | AsyncGenerator[Event, None]:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
"""Run tasks either concurrently (if independent) or sequentially via orchestrator."""
|
|
41
|
+
# If we have multiple independent tasks, run them concurrently
|
|
42
|
+
if len(tasks) > 1 and _tasks_are_independent(tasks):
|
|
43
|
+
# Run independent tasks concurrently using asyncio.gather
|
|
44
|
+
await asyncio.gather(*[task.run_async() for task in tasks])
|
|
45
|
+
return tasks
|
|
46
|
+
else:
|
|
47
|
+
# Use orchestrator for dependent tasks or single tasks
|
|
48
|
+
orchestrator = Orchestrator(
|
|
49
|
+
tasks=tasks,
|
|
50
|
+
thread=thread,
|
|
51
|
+
handlers=handlers,
|
|
52
|
+
)
|
|
53
|
+
await orchestrator.run(raise_on_failure=raise_on_failure)
|
|
54
|
+
return tasks
|
|
31
55
|
|
|
32
56
|
|
|
33
57
|
async def run_tasks_stream(
|
|
@@ -79,6 +103,7 @@ def run_tasks(
|
|
|
79
103
|
raise_on_failure: bool = True,
|
|
80
104
|
handlers: list[Handler | AsyncHandler] | None = None,
|
|
81
105
|
) -> list[Task[Any]]:
|
|
106
|
+
"""Run tasks either concurrently (if independent) or sequentially."""
|
|
82
107
|
return marvin.utilities.asyncio.run_sync(
|
|
83
108
|
run_tasks_async(
|
|
84
109
|
tasks=tasks,
|
|
@@ -20,6 +20,7 @@ from typing import (
|
|
|
20
20
|
Sequence,
|
|
21
21
|
TypeAlias,
|
|
22
22
|
TypeVar,
|
|
23
|
+
get_args,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
from pydantic import TypeAdapter
|
|
@@ -452,7 +453,10 @@ class Task(Generic[T]):
|
|
|
452
453
|
else:
|
|
453
454
|
raise ValueError("Result type is None but result is not None")
|
|
454
455
|
elif raw_result is None:
|
|
455
|
-
|
|
456
|
+
# Check if None is allowed in the type (e.g., Optional[str] = str | None)
|
|
457
|
+
type_args = get_args(result_type)
|
|
458
|
+
if type(None) not in type_args:
|
|
459
|
+
raise ValueError("Result is None but result type is not None")
|
|
456
460
|
|
|
457
461
|
type_adapter = get_type_adapter(result_type)
|
|
458
462
|
return type_adapter.validate_python(raw_result)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from marvin import Task
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestTaskPrompt:
|
|
5
|
+
async def test_task_return_string_prompt(self):
|
|
6
|
+
# default tasks return a list; ensure strings work as well
|
|
7
|
+
class MyTask(Task):
|
|
8
|
+
def get_prompt(self) -> str:
|
|
9
|
+
return 'Say the word "hello"'
|
|
10
|
+
|
|
11
|
+
task = MyTask("")
|
|
12
|
+
prompt = task.get_prompt()
|
|
13
|
+
assert prompt == 'Say the word "hello"'
|
|
14
|
+
assert task.run() == "hello"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestOptionalResults:
|
|
18
|
+
async def test_optional_result_returns_none(self):
|
|
19
|
+
"""Test that tasks with optional result types can actually return None from AI."""
|
|
20
|
+
task = Task(
|
|
21
|
+
instructions="return null",
|
|
22
|
+
result_type=str | None,
|
|
23
|
+
)
|
|
24
|
+
result = await task.run_async()
|
|
25
|
+
assert result is None
|
|
26
|
+
|
|
27
|
+
async def test_optional_result_returns_value(self):
|
|
28
|
+
"""Test that optional tasks can also return actual values."""
|
|
29
|
+
task = Task(
|
|
30
|
+
instructions='say "hello"',
|
|
31
|
+
result_type=str | None,
|
|
32
|
+
)
|
|
33
|
+
result = await task.run_async()
|
|
34
|
+
assert result == "hello"
|
|
@@ -258,6 +258,13 @@ async def test_task_mark_successful():
|
|
|
258
258
|
assert task.result == "test result"
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
async def test_task_optional_result_type():
|
|
262
|
+
"""Test that tasks with Optional result types can validate None."""
|
|
263
|
+
task = Task(instructions="test", result_type=str | None)
|
|
264
|
+
validated = task.validate_result(None)
|
|
265
|
+
assert validated is None
|
|
266
|
+
|
|
267
|
+
|
|
261
268
|
async def test_task_mark_failed():
|
|
262
269
|
"""Test marking task as failed."""
|
|
263
270
|
task = Task(instructions="Test failure")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Unit tests for concurrent task execution."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from marvin import Task
|
|
8
|
+
from marvin.agents.agent import Agent
|
|
9
|
+
from marvin.fns.run import _tasks_are_independent, run_tasks, run_tasks_async
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTaskIndependenceDetection:
|
|
13
|
+
"""Test the independence detection logic."""
|
|
14
|
+
|
|
15
|
+
def test_independent_tasks(self):
|
|
16
|
+
"""Test that truly independent tasks are detected as such."""
|
|
17
|
+
task1 = Task("Say 'one'", result_type=str)
|
|
18
|
+
task2 = Task("Say 'two'", result_type=str)
|
|
19
|
+
task3 = Task("Say 'three'", result_type=str)
|
|
20
|
+
|
|
21
|
+
assert _tasks_are_independent([task1, task2, task3])
|
|
22
|
+
|
|
23
|
+
def test_dependent_tasks_depends_on(self):
|
|
24
|
+
"""Test that tasks with depends_on are not independent."""
|
|
25
|
+
task1 = Task("Say 'one'", result_type=str)
|
|
26
|
+
task2 = Task("Say 'two'", result_type=str, depends_on=[task1])
|
|
27
|
+
|
|
28
|
+
assert not _tasks_are_independent([task1, task2])
|
|
29
|
+
|
|
30
|
+
def test_dependent_tasks_parent_child(self):
|
|
31
|
+
"""Test that parent-child tasks are not independent."""
|
|
32
|
+
parent = Task("Parent task", result_type=str)
|
|
33
|
+
child = Task("Child task", result_type=str)
|
|
34
|
+
parent.subtasks.add(child) # subtasks is a set, not list
|
|
35
|
+
child.parent = parent
|
|
36
|
+
|
|
37
|
+
assert not _tasks_are_independent([parent, child])
|
|
38
|
+
|
|
39
|
+
def test_single_task_is_independent(self):
|
|
40
|
+
"""Test that a single task is considered independent."""
|
|
41
|
+
task = Task("Solo task", result_type=str)
|
|
42
|
+
assert _tasks_are_independent([task])
|
|
43
|
+
|
|
44
|
+
def test_empty_task_list(self):
|
|
45
|
+
"""Test empty task list."""
|
|
46
|
+
assert _tasks_are_independent([])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestConcurrentExecution:
|
|
50
|
+
"""Test actual concurrent execution behavior."""
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_independent_tasks_run_without_errors(self):
|
|
54
|
+
"""Test that independent tasks run without Multiple EndTurn warnings or errors."""
|
|
55
|
+
task1 = Task("Say 'one'", result_type=str)
|
|
56
|
+
task2 = Task("Say 'two'", result_type=str)
|
|
57
|
+
task3 = Task("Say 'three'", result_type=str)
|
|
58
|
+
|
|
59
|
+
# Should not raise any errors (no Multiple EndTurn warnings, no infinite loops)
|
|
60
|
+
results = await run_tasks_async([task1, task2, task3])
|
|
61
|
+
|
|
62
|
+
assert len(results) == 3
|
|
63
|
+
assert all(task.is_successful() for task in results)
|
|
64
|
+
|
|
65
|
+
# Verify results
|
|
66
|
+
result_values = [task.result for task in results]
|
|
67
|
+
assert set(result_values) == {"one", "two", "three"}
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_dependent_tasks_run_in_order(self):
|
|
71
|
+
"""Test that dependent tasks run in correct order."""
|
|
72
|
+
task1 = Task("Say 'A'", result_type=str)
|
|
73
|
+
task2 = Task("Say 'B'", result_type=str, depends_on=[task1])
|
|
74
|
+
task3 = Task("Say 'C'", result_type=str, depends_on=[task2])
|
|
75
|
+
|
|
76
|
+
results = await run_tasks_async([task1, task2, task3])
|
|
77
|
+
|
|
78
|
+
assert len(results) == 3
|
|
79
|
+
assert all(task.is_successful() for task in results)
|
|
80
|
+
|
|
81
|
+
# Verify correct order
|
|
82
|
+
result_values = [task.result for task in results]
|
|
83
|
+
assert result_values == ["A", "B", "C"]
|
|
84
|
+
|
|
85
|
+
def test_sync_run_tasks_independent(self):
|
|
86
|
+
"""Test synchronous run_tasks with independent tasks."""
|
|
87
|
+
task1 = Task("Say 'one'", result_type=str)
|
|
88
|
+
task2 = Task("Say 'two'", result_type=str)
|
|
89
|
+
|
|
90
|
+
results = run_tasks([task1, task2])
|
|
91
|
+
|
|
92
|
+
assert len(results) == 2
|
|
93
|
+
assert all(task.is_successful() for task in results)
|
|
94
|
+
assert set(t.result for t in results) == {"one", "two"}
|
|
95
|
+
|
|
96
|
+
def test_sync_run_tasks_dependent(self):
|
|
97
|
+
"""Test synchronous run_tasks with dependent tasks."""
|
|
98
|
+
task1 = Task("Say 'A'", result_type=str)
|
|
99
|
+
task2 = Task("Say 'B'", result_type=str, depends_on=[task1])
|
|
100
|
+
|
|
101
|
+
results = run_tasks([task1, task2])
|
|
102
|
+
|
|
103
|
+
assert len(results) == 2
|
|
104
|
+
assert all(task.is_successful() for task in results)
|
|
105
|
+
|
|
106
|
+
# Verify correct order
|
|
107
|
+
result_values = [task.result for task in results]
|
|
108
|
+
assert result_values == ["A", "B"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestAsyncioGatherCompatibility:
|
|
112
|
+
"""Test that asyncio.gather works without ContextVar errors."""
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
async def test_asyncio_gather_no_context_errors(self):
|
|
116
|
+
"""Test that asyncio.gather doesn't throw ContextVar errors."""
|
|
117
|
+
task1 = Task("Say 'async1'", result_type=str)
|
|
118
|
+
task2 = Task("Say 'async2'", result_type=str)
|
|
119
|
+
task3 = Task("Say 'async3'", result_type=str)
|
|
120
|
+
|
|
121
|
+
# This should not raise ContextVar token errors
|
|
122
|
+
results = await asyncio.gather(
|
|
123
|
+
task1.run_async(), task2.run_async(), task3.run_async()
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert len(results) == 3
|
|
127
|
+
assert set(results) == {"async1", "async2", "async3"}
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_mixed_execution_patterns(self):
|
|
131
|
+
"""Test mixing run_tasks_async and asyncio.gather in same event loop."""
|
|
132
|
+
# First batch via run_tasks_async
|
|
133
|
+
task1 = Task("Say 'batch1'", result_type=str)
|
|
134
|
+
task2 = Task("Say 'batch2'", result_type=str)
|
|
135
|
+
batch1_results = await run_tasks_async([task1, task2])
|
|
136
|
+
|
|
137
|
+
# Second batch via asyncio.gather
|
|
138
|
+
task3 = Task("Say 'gather1'", result_type=str)
|
|
139
|
+
task4 = Task("Say 'gather2'", result_type=str)
|
|
140
|
+
batch2_results = await asyncio.gather(task3.run_async(), task4.run_async())
|
|
141
|
+
|
|
142
|
+
# Both should work without errors
|
|
143
|
+
assert len(batch1_results) == 2
|
|
144
|
+
assert all(task.is_successful() for task in batch1_results)
|
|
145
|
+
assert len(batch2_results) == 2
|
|
146
|
+
assert set(batch2_results) == {"gather1", "gather2"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestContextVarHandling:
|
|
150
|
+
"""Test ContextVar token handling across async contexts."""
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
async def test_actor_context_across_asyncio_gather(self):
|
|
154
|
+
"""Test that Actor context management handles asyncio.gather correctly."""
|
|
155
|
+
from marvin.agents.actor import _current_actor
|
|
156
|
+
|
|
157
|
+
async def task_with_actor(name):
|
|
158
|
+
actor = Agent(name=f"Agent_{name}")
|
|
159
|
+
# This should not raise an error even with asyncio.gather
|
|
160
|
+
with actor:
|
|
161
|
+
assert _current_actor.get() == actor
|
|
162
|
+
await asyncio.sleep(0.1) # Simulate async work
|
|
163
|
+
# Context should be reset without errors
|
|
164
|
+
return name
|
|
165
|
+
|
|
166
|
+
# Test that concurrent context management works
|
|
167
|
+
results = await asyncio.gather(
|
|
168
|
+
task_with_actor("1"), task_with_actor("2"), task_with_actor("3")
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
assert results == ["1", "2", "3"]
|
|
172
|
+
# Context should be None after all tasks complete
|
|
173
|
+
assert _current_actor.get() is None
|
|
174
|
+
|
|
175
|
+
def test_actor_context_sequential(self):
|
|
176
|
+
"""Test that Actor context works normally in sequential execution."""
|
|
177
|
+
from marvin.agents.actor import _current_actor
|
|
178
|
+
|
|
179
|
+
actor1 = Agent(name="Sequential_1")
|
|
180
|
+
actor2 = Agent(name="Sequential_2")
|
|
181
|
+
|
|
182
|
+
# Test nested contexts work correctly
|
|
183
|
+
assert _current_actor.get() is None
|
|
184
|
+
|
|
185
|
+
with actor1:
|
|
186
|
+
assert _current_actor.get() == actor1
|
|
187
|
+
with actor2:
|
|
188
|
+
assert _current_actor.get() == actor2
|
|
189
|
+
assert _current_actor.get() == actor1
|
|
190
|
+
|
|
191
|
+
assert _current_actor.get() is None
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from marvin import Task
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class TestTaskPrompt:
|
|
5
|
-
async def test_task_return_string_prompt(self):
|
|
6
|
-
# default tasks return a list; ensure strings work as well
|
|
7
|
-
class MyTask(Task):
|
|
8
|
-
def get_prompt(self) -> str:
|
|
9
|
-
return 'Say the word "hello"'
|
|
10
|
-
|
|
11
|
-
task = MyTask("")
|
|
12
|
-
prompt = task.get_prompt()
|
|
13
|
-
assert prompt == 'Say the word "hello"'
|
|
14
|
-
assert task.run() == "hello"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|