reactifact 0.14.0__tar.gz → 0.14.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.
- {reactifact-0.14.0/reactifact.egg-info → reactifact-0.14.2}/PKG-INFO +1 -1
- {reactifact-0.14.0 → reactifact-0.14.2}/pyproject.toml +1 -1
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/__init__.py +1 -1
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/chat.py +14 -1
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/chat.py +5 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/runtime.py +3 -3
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/sessions.html +6 -6
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/ui.html +14 -14
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/ui_run.html +10 -10
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/web.py +49 -13
- {reactifact-0.14.0 → reactifact-0.14.2/reactifact.egg-info}/PKG-INFO +1 -1
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/SOURCES.txt +1 -0
- reactifact-0.14.2/tests/test_chat_outbox.py +70 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_tracing.py +84 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/LICENSE +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/README.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/__main__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/_extras.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/_httpx.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/client.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/models.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/remote.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/server.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/agent_tool.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/agents.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/artifacts.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/audit.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/authz.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/branching.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/budget.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cache.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/checkpoints.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/branch.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/common.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/context.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/eval.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/graph.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/replay.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/skills.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/trace.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/commit.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/commit_log.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/consume.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/context.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/context_builder.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/effects.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/dataset.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/evaluators.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/judge.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/online.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/runner.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/scoring.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/events.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/guardrails.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/interrupt.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/llm_agent.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/logging.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/client.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/server.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/metrics.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/operations.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/patches.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/pricing.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/produce.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/prompts.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/azure.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/chat.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/fake.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/groq.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/image.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/openai.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/speech.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/together.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/video.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/xai.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/zai.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/py.typed +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/agent.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/models.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/rag.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quota.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/conversation.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/search.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/status.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/text.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/tool_loop.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/redaction.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/relations.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/replay.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/request.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/resources.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/routing.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/scheduler.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/session.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-eval/references/online.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/sources.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/streaming.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/structured.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/fault.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/golden.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/lab.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/mock.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/pytest_plugin.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/record.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/registry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tool_use.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tools.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/__init__.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/columns.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/models.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/store.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/app.css +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/tracer.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/triggers.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/types.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/verify.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/viz.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/setup.cfg +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a_example.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a_interop.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_absent_consume.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_adaptive.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_agent_tool.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_artifacts.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_audit.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_authz.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_backbone.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_branching.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_budget.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cache.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_example.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_memory.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_checkpoint.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cli.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_commit_log.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_compaction.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_concurrency.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_context_builder.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_debounce.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_determinism.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops_online.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_effects.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_dataset.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_judge.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_online.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_trajectory.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_forklab.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_forklab_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_friendly_api.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_golden.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_guardrails.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_hitl.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_image_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_incident_commander.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_invalidation.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_join_consume.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_knowledge.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_logging.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_mcp.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_medic_lab.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_metrics.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_multisource.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_openai_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_outbox.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_patches.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_pending_actions.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_ports.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_produce_styles.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_prompts.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_provider_auth.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_provider_retry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_providers_integration.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_public_api.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_quick.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_quota.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_reacts_to.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_conversation.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_tool_loop.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_redaction.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_relation_graph.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_relations.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repair.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repair_web.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_replay.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repo_agent.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_request.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_research.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_resources.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_retry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_routing.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_runtime.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sessions.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_shutdown.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_skills.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources_search.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources_vector.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_speech_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_starter_app.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_streaming.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_structured.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_support_copilot.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_golden.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_lab.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_mock.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_provenance.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_pytest_plugin.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_registry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_token_budget.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_tools.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_trace_columns.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_trace_tags.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_type_registry.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_verify.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_video_provider.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_view.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_viz.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_web_source.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_workspace.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.14.0 → reactifact-0.14.2}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
4
4
|
Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/bzdvdn/reactifact
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.14.
|
|
7
|
+
version = "0.14.2"
|
|
8
8
|
description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -38,7 +38,7 @@ from .consume import Consume
|
|
|
38
38
|
from .context import Context
|
|
39
39
|
from .events import Event
|
|
40
40
|
from .resources import RuntimeResources
|
|
41
|
-
from .runtime import Runtime
|
|
41
|
+
from .runtime import Dispatcher, DispatchErrorHandler, Runtime
|
|
42
42
|
from .session import Session, SessionStore
|
|
43
43
|
|
|
44
44
|
logger = logging.getLogger("reactifact.chat")
|
|
@@ -306,6 +306,13 @@ class ChatAssistant:
|
|
|
306
306
|
multi-stage pipeline routinely produces several generations per turn (see
|
|
307
307
|
`Runtime.__init__`'s own docstring for the trade-off).
|
|
308
308
|
|
|
309
|
+
`dispatcher=` (and `on_dispatch_error=`) also passes straight through to
|
|
310
|
+
`Runtime`: give an agent that uses `effects.act(...)` a way to actually send
|
|
311
|
+
its outbound side effects from a chat turn, with the same once-per-stable-id
|
|
312
|
+
delivery `Runtime(dispatcher=...)` provides. Without it, a chat turn that
|
|
313
|
+
records a `PendingAction` leaves it `pending` (the runtime warns) for someone
|
|
314
|
+
to drain later.
|
|
315
|
+
|
|
309
316
|
`resources=`/`create_message=` may optionally take the current turn's
|
|
310
317
|
`session_id` — `resources=lambda session_id: build_resources(session_id)`
|
|
311
318
|
(e.g. to attach an authenticated user looked up from the session) and
|
|
@@ -349,6 +356,8 @@ class ChatAssistant:
|
|
|
349
356
|
on_agent_error: Callable[[Agent, Event, BaseException], None] | None = None,
|
|
350
357
|
session_save_policy: Literal["per_commit", "per_turn"] = "per_commit",
|
|
351
358
|
memory: ChatMemory | None = None,
|
|
359
|
+
dispatcher: Dispatcher | None = None,
|
|
360
|
+
on_dispatch_error: DispatchErrorHandler | None = None,
|
|
352
361
|
):
|
|
353
362
|
self.store = store
|
|
354
363
|
self._agents = agents
|
|
@@ -368,6 +377,8 @@ class ChatAssistant:
|
|
|
368
377
|
session_save_policy
|
|
369
378
|
)
|
|
370
379
|
self._memory = memory
|
|
380
|
+
self._dispatcher = dispatcher
|
|
381
|
+
self._on_dispatch_error = on_dispatch_error
|
|
371
382
|
# Serializes concurrent turns on the *same* session_id (a double
|
|
372
383
|
# submit, a client retry): without this, two overlapping stream()
|
|
373
384
|
# calls both load the same starting state and the later save() wins,
|
|
@@ -453,6 +464,8 @@ class ChatAssistant:
|
|
|
453
464
|
isolate_errors=self._isolate_errors,
|
|
454
465
|
on_agent_error=self._on_agent_error,
|
|
455
466
|
session_save_policy=self._session_save_policy,
|
|
467
|
+
dispatcher=self._dispatcher,
|
|
468
|
+
on_dispatch_error=self._on_dispatch_error,
|
|
456
469
|
)
|
|
457
470
|
|
|
458
471
|
async def stream(
|
|
@@ -15,6 +15,7 @@ from ..checkpoints import FileKVBackend, InMemoryKVBackend
|
|
|
15
15
|
from ..context import Context
|
|
16
16
|
from ..providers import LLMProvider, from_env
|
|
17
17
|
from ..resources import RuntimeResources
|
|
18
|
+
from ..runtime import Dispatcher, DispatchErrorHandler
|
|
18
19
|
from ..session import SessionStore
|
|
19
20
|
from .models import Question
|
|
20
21
|
|
|
@@ -54,6 +55,8 @@ def chat_agent(
|
|
|
54
55
|
resources: RuntimeResources | Callable[..., RuntimeResources] | None = None,
|
|
55
56
|
budget: Budget | None = None,
|
|
56
57
|
tracer: Any = None,
|
|
58
|
+
dispatcher: Dispatcher | None = None,
|
|
59
|
+
on_dispatch_error: DispatchErrorHandler | None = None,
|
|
57
60
|
) -> ChatAssistant:
|
|
58
61
|
"""A configured `ChatAssistant` — sessions, turns, history, out of the box.
|
|
59
62
|
|
|
@@ -79,6 +82,8 @@ def chat_agent(
|
|
|
79
82
|
resources=resources,
|
|
80
83
|
budget=budget,
|
|
81
84
|
tracer=tracer,
|
|
85
|
+
dispatcher=dispatcher,
|
|
86
|
+
on_dispatch_error=on_dispatch_error,
|
|
82
87
|
)
|
|
83
88
|
|
|
84
89
|
|
|
@@ -38,6 +38,8 @@ AgentResult = tuple[Patch | None, Agent, Event, list[Read], float, BaseException
|
|
|
38
38
|
#: the intent is committed; the app performs the real I/O and returns (or
|
|
39
39
|
#: raises). Must be idempotent on `idempotency_key` (§42).
|
|
40
40
|
Dispatcher = Callable[[Context, Artifact[PendingAction]], Awaitable[None]]
|
|
41
|
+
#: Called when the dispatcher raises (the action is already marked `failed`).
|
|
42
|
+
DispatchErrorHandler = Callable[[Artifact[PendingAction], BaseException], None]
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
@dataclass
|
|
@@ -93,9 +95,7 @@ class Runtime:
|
|
|
93
95
|
on_agent_error: Callable[[Agent, Event, BaseException], None] | None = None,
|
|
94
96
|
session_save_policy: Literal["per_commit", "per_turn"] = "per_commit",
|
|
95
97
|
dispatcher: Dispatcher | None = None,
|
|
96
|
-
on_dispatch_error:
|
|
97
|
-
Callable[[Artifact[PendingAction], BaseException], None] | None
|
|
98
|
-
) = None,
|
|
98
|
+
on_dispatch_error: DispatchErrorHandler | None = None,
|
|
99
99
|
):
|
|
100
100
|
self.context = context
|
|
101
101
|
self.agents = agents or []
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
<meta charset="utf-8"/>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
6
|
<title>reactifact sessions</title>
|
|
7
|
-
<link rel="stylesheet" href="/traces/assets/app.css"/>
|
|
7
|
+
<link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
|
|
8
8
|
<script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div class="app" id="app">
|
|
12
12
|
<aside class="sidebar">
|
|
13
|
-
<a class="brand" href="/traces">
|
|
13
|
+
<a class="brand" href="__BASE__/traces">
|
|
14
14
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
15
15
|
<span class="name">reactifact <em>traces</em></span>
|
|
16
16
|
</a>
|
|
17
17
|
<nav class="nav">
|
|
18
|
-
<a class="nav-item" href="/traces">
|
|
18
|
+
<a class="nav-item" href="__BASE__/traces">
|
|
19
19
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
|
|
20
20
|
<span class="nav-label">Traces</span>
|
|
21
21
|
</a>
|
|
22
|
-
<a class="nav-item active" href="/sessions">
|
|
22
|
+
<a class="nav-item active" href="__BASE__/sessions">
|
|
23
23
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
|
|
24
24
|
<span class="nav-label">Sessions</span>
|
|
25
25
|
</a>
|
|
@@ -97,7 +97,7 @@ async function load(){
|
|
|
97
97
|
const p=new URLSearchParams({limit:state.limit,offset:state.offset});
|
|
98
98
|
if(state.q)p.set('q',state.q);
|
|
99
99
|
try{
|
|
100
|
-
const res=await fetch('/api/sessions?'+p);
|
|
100
|
+
const res=await fetch('__BASE__/api/sessions?'+p);
|
|
101
101
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
|
102
102
|
const data=await res.json();
|
|
103
103
|
state.items=data.items||[];state.total=data.total||0;render();
|
|
@@ -133,7 +133,7 @@ function updatePager(){
|
|
|
133
133
|
}
|
|
134
134
|
document.getElementById('rows').addEventListener('click',(e)=>{
|
|
135
135
|
const row=e.target.closest('tr.row');if(!row)return;
|
|
136
|
-
window.location.href='/traces?session_id='+encodeURIComponent(row.dataset.id);
|
|
136
|
+
window.location.href='__BASE__/traces?session_id='+encodeURIComponent(row.dataset.id);
|
|
137
137
|
});
|
|
138
138
|
document.getElementById('f-q').addEventListener('input',debounce((e)=>{state.q=e.target.value.trim();state.offset=0;load();}));
|
|
139
139
|
document.getElementById('prev').addEventListener('click',()=>{state.offset=Math.max(0,state.offset-state.limit);load();});
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
<meta charset="utf-8"/>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
6
|
<title>reactifact traces</title>
|
|
7
|
-
<link rel="stylesheet" href="/traces/assets/app.css"/>
|
|
7
|
+
<link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
|
|
8
8
|
<script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div class="app" id="app">
|
|
12
12
|
<aside class="sidebar">
|
|
13
|
-
<a class="brand" href="/traces">
|
|
13
|
+
<a class="brand" href="__BASE__/traces">
|
|
14
14
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
15
15
|
<span class="name">reactifact <em>traces</em></span>
|
|
16
16
|
</a>
|
|
17
17
|
<nav class="nav">
|
|
18
|
-
<a class="nav-item active" href="/traces">
|
|
18
|
+
<a class="nav-item active" href="__BASE__/traces">
|
|
19
19
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
|
|
20
20
|
<span class="nav-label">Traces</span>
|
|
21
21
|
</a>
|
|
22
|
-
<a class="nav-item" href="/sessions">
|
|
22
|
+
<a class="nav-item" href="__BASE__/sessions">
|
|
23
23
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
|
|
24
24
|
<span class="nav-label">Sessions</span>
|
|
25
25
|
</a>
|
|
@@ -188,7 +188,7 @@ function loadColumnPref(){try{const raw=localStorage.getItem(COLS_KEY);VISIBLE_C
|
|
|
188
188
|
function saveColumnPref(){try{localStorage.setItem(COLS_KEY,JSON.stringify(VISIBLE_COLS||[]));}catch(_){}}
|
|
189
189
|
function visibleColumns(){const all=state.columns.map(c=>c.label);if(VISIBLE_COLS===null)return all;return all.filter(l=>VISIBLE_COLS.includes(l));}
|
|
190
190
|
async function loadColumns(){
|
|
191
|
-
try{const r=await fetch('/api/columns');state.columns=(await r.json()).items||[];}
|
|
191
|
+
try{const r=await fetch('__BASE__/api/columns');state.columns=(await r.json()).items||[];}
|
|
192
192
|
catch(_){state.columns=[];}
|
|
193
193
|
render();
|
|
194
194
|
}
|
|
@@ -287,7 +287,7 @@ function filters(){
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
async function loadFacets(){
|
|
290
|
-
try{const r=await fetch('/api/tags');const d=await r.json();state.facets=d.items||[];}
|
|
290
|
+
try{const r=await fetch('__BASE__/api/tags');const d=await r.json();state.facets=d.items||[];}
|
|
291
291
|
catch(_){state.facets=[];}
|
|
292
292
|
renderFacets();
|
|
293
293
|
}
|
|
@@ -308,7 +308,7 @@ function renderFacets(){
|
|
|
308
308
|
async function load(){
|
|
309
309
|
const q=filters();q.set('limit',state.limit);q.set('offset',state.offset);
|
|
310
310
|
try{
|
|
311
|
-
const res=await fetch('/api/traces?'+q);
|
|
311
|
+
const res=await fetch('__BASE__/api/traces?'+q);
|
|
312
312
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
|
313
313
|
const data=await res.json();
|
|
314
314
|
state.runs=data.items||[];state.total=data.total||0;state.stats=data.stats||null;
|
|
@@ -454,7 +454,7 @@ document.getElementById('view-del').addEventListener('click',()=>{
|
|
|
454
454
|
const sel=document.getElementById('views');if(sel.value==='')return;
|
|
455
455
|
const views=getViews();views.splice(Number(sel.value),1);setViews(views);renderViews();toast('View deleted');
|
|
456
456
|
});
|
|
457
|
-
document.getElementById('export').addEventListener('click',()=>{const q=filters();q.set('limit',500);window.location.href='/api/traces/export?'+q;});
|
|
457
|
+
document.getElementById('export').addEventListener('click',()=>{const q=filters();q.set('limit',500);window.location.href='__BASE__/api/traces/export?'+q;});
|
|
458
458
|
document.getElementById('live').addEventListener('click',(e)=>{
|
|
459
459
|
state.live=!state.live;e.currentTarget.classList.toggle('on',state.live);
|
|
460
460
|
if(state.live){timer=setInterval(()=>{if(!document.hidden)load();},3000);}else{clearInterval(timer);}
|
|
@@ -462,7 +462,7 @@ document.getElementById('live').addEventListener('click',(e)=>{
|
|
|
462
462
|
document.getElementById('runs').addEventListener('click',(e)=>{
|
|
463
463
|
if(e.target.closest('.rowcheck'))return;
|
|
464
464
|
const row=e.target.closest('tr.row');if(!row)return;
|
|
465
|
-
window.location.href='/traces/'+row.dataset.id;
|
|
465
|
+
window.location.href='__BASE__/traces/'+row.dataset.id;
|
|
466
466
|
});
|
|
467
467
|
document.getElementById('runs').addEventListener('change',(e)=>{
|
|
468
468
|
const c=e.target.closest('.rowcheck');if(!c)return;
|
|
@@ -476,13 +476,13 @@ document.getElementById('bulk-clear').addEventListener('click',()=>{state.select
|
|
|
476
476
|
document.getElementById('bulk-apply').addEventListener('click',async()=>{
|
|
477
477
|
const tag=document.getElementById('bulk-tag').value.trim();if(!tag)return;
|
|
478
478
|
const note=document.getElementById('bulk-note').value;
|
|
479
|
-
await fetch('/api/traces/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag,note:note})});
|
|
479
|
+
await fetch('__BASE__/api/traces/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag,note:note})});
|
|
480
480
|
document.getElementById('bulk-tag').value='';document.getElementById('bulk-note').value='';
|
|
481
481
|
state.selected.clear();toast('Tagged');await refresh();
|
|
482
482
|
});
|
|
483
483
|
document.getElementById('bulk-remove').addEventListener('click',async()=>{
|
|
484
484
|
const tag=document.getElementById('bulk-remove-sel').value;if(!tag)return;
|
|
485
|
-
await fetch('/api/traces/tags/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag})});
|
|
485
|
+
await fetch('__BASE__/api/traces/tags/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag})});
|
|
486
486
|
state.selected.clear();toast('Tag removed');await refresh();
|
|
487
487
|
});
|
|
488
488
|
|
|
@@ -507,18 +507,18 @@ document.getElementById('tagrows').addEventListener('click',async(e)=>{
|
|
|
507
507
|
if(e.target.closest('.row-save')){
|
|
508
508
|
const name=row.querySelector('.row-name').value.trim();
|
|
509
509
|
const color=row.querySelector('.row-color').value;
|
|
510
|
-
await fetch('/api/tags/'+encodeURIComponent(old),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({new_name:name,color:color})});
|
|
510
|
+
await fetch('__BASE__/api/tags/'+encodeURIComponent(old),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({new_name:name,color:color})});
|
|
511
511
|
await refresh();renderTagRows();toast('Tag updated');
|
|
512
512
|
}else if(e.target.closest('.row-del')){
|
|
513
513
|
if(!confirm('Delete tag "'+old+'" from every run?'))return;
|
|
514
|
-
await fetch('/api/tags/'+encodeURIComponent(old),{method:'DELETE'});
|
|
514
|
+
await fetch('__BASE__/api/tags/'+encodeURIComponent(old),{method:'DELETE'});
|
|
515
515
|
await refresh();renderTagRows();toast('Tag deleted');
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
document.getElementById('new-tag-add').addEventListener('click',async()=>{
|
|
519
519
|
const name=document.getElementById('new-tag-name').value.trim();if(!name)return;
|
|
520
520
|
const color=document.getElementById('new-tag-color').value;
|
|
521
|
-
await fetch('/api/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,color:color})});
|
|
521
|
+
await fetch('__BASE__/api/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,color:color})});
|
|
522
522
|
document.getElementById('new-tag-name').value='';await refresh();renderTagRows();toast('Tag added');
|
|
523
523
|
});
|
|
524
524
|
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
<meta charset="utf-8"/>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
6
|
<title>reactifact trace</title>
|
|
7
|
-
<link rel="stylesheet" href="/traces/assets/app.css"/>
|
|
7
|
+
<link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
|
|
8
8
|
<script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div class="app" id="app">
|
|
12
12
|
<aside class="sidebar">
|
|
13
|
-
<a class="brand" href="/traces">
|
|
13
|
+
<a class="brand" href="__BASE__/traces">
|
|
14
14
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
15
15
|
<span class="name">reactifact <em>traces</em></span>
|
|
16
16
|
</a>
|
|
17
17
|
<nav class="nav">
|
|
18
|
-
<a class="nav-item active" href="/traces">
|
|
18
|
+
<a class="nav-item active" href="__BASE__/traces">
|
|
19
19
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
|
|
20
20
|
<span class="nav-label">Traces</span>
|
|
21
21
|
</a>
|
|
22
|
-
<a class="nav-item" href="/sessions">
|
|
22
|
+
<a class="nav-item" href="__BASE__/sessions">
|
|
23
23
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
|
|
24
24
|
<span class="nav-label">Sessions</span>
|
|
25
25
|
</a>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<button class="icon-btn" id="menu-btn" aria-label="open navigation">
|
|
43
43
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
44
44
|
</button>
|
|
45
|
-
<div class="crumb"><a href="/traces">traces</a><span class="sep">/</span><b id="cid">#…</b><span id="head"></span></div>
|
|
45
|
+
<div class="crumb"><a href="__BASE__/traces">traces</a><span class="sep">/</span><b id="cid">#…</b><span id="head"></span></div>
|
|
46
46
|
<div class="spacer"></div>
|
|
47
47
|
<button class="btn sm" id="copy-link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.1 0l3-3a5 5 0 0 0-7.1-7.1L11 4.9"/><path d="M14 11a5 5 0 0 0-7.1 0l-3 3A5 5 0 0 0 11 21.1l1.9-1.9"/></svg><span data-label>link</span></button>
|
|
48
48
|
<button class="btn sm" id="copy-json"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span data-label>json</span></button>
|
|
@@ -104,7 +104,7 @@ function toast(msg){let t=document.getElementById('toast');if(!t){t=document.cre
|
|
|
104
104
|
|
|
105
105
|
async function load(){
|
|
106
106
|
try{
|
|
107
|
-
const res=await fetch('/api/traces/'+RUN_ID);
|
|
107
|
+
const res=await fetch('__BASE__/api/traces/'+RUN_ID);
|
|
108
108
|
if(res.status===404){document.getElementById('cid').textContent='#'+RUN_ID+' — not found';return;}
|
|
109
109
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
|
110
110
|
render(await res.json());
|
|
@@ -122,7 +122,7 @@ function hex(c){return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(
|
|
|
122
122
|
let VOCAB=[];
|
|
123
123
|
let CURRENT=[];
|
|
124
124
|
async function loadVocab(){
|
|
125
|
-
try{const r=await fetch('/api/tags');const d=await r.json();VOCAB=(d.items||[]).map(x=>x.name);}
|
|
125
|
+
try{const r=await fetch('__BASE__/api/tags');const d=await r.json();VOCAB=(d.items||[]).map(x=>x.name);}
|
|
126
126
|
catch(_){VOCAB=[];}
|
|
127
127
|
const dl=document.getElementById('taglist');
|
|
128
128
|
if(dl)dl.innerHTML=VOCAB.map(n=>'<option value="'+escapeHtml(n)+'">').join('');
|
|
@@ -152,18 +152,18 @@ function renderAnnot(list){
|
|
|
152
152
|
document.getElementById('annot-note').addEventListener('keydown',(e)=>{if(e.key==='Enter'){e.preventDefault();addTag();}});
|
|
153
153
|
}
|
|
154
154
|
async function reloadTrace(){
|
|
155
|
-
const res=await fetch('/api/traces/'+RUN_ID);
|
|
155
|
+
const res=await fetch('__BASE__/api/traces/'+RUN_ID);
|
|
156
156
|
if(res.ok)renderAnnot((await res.json()).annotations||[]);
|
|
157
157
|
await loadVocab();
|
|
158
158
|
}
|
|
159
159
|
async function addTag(){
|
|
160
160
|
const tag=document.getElementById('annot-tag').value.trim();if(!tag)return;
|
|
161
161
|
const note=document.getElementById('annot-note').value;
|
|
162
|
-
await fetch('/api/traces/'+RUN_ID+'/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tag:tag,note:note})});
|
|
162
|
+
await fetch('__BASE__/api/traces/'+RUN_ID+'/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tag:tag,note:note})});
|
|
163
163
|
toast('Tag added');await reloadTrace();
|
|
164
164
|
}
|
|
165
165
|
async function removeTag(tag){
|
|
166
|
-
await fetch('/api/traces/'+RUN_ID+'/tags/'+encodeURIComponent(tag),{method:'DELETE'});
|
|
166
|
+
await fetch('__BASE__/api/traces/'+RUN_ID+'/tags/'+encodeURIComponent(tag),{method:'DELETE'});
|
|
167
167
|
toast('Tag removed');await reloadTrace();
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -33,11 +33,41 @@ from .columns import TraceColumn
|
|
|
33
33
|
from .store import TraceStoreProtocol
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
|
-
from fastapi import APIRouter
|
|
36
|
+
from fastapi import APIRouter, Request
|
|
37
37
|
|
|
38
38
|
_TEMPLATES = Path(__file__).parent / "templates"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _mount_base(request: Request, logical_path: str) -> str:
|
|
42
|
+
"""Path prefix this router is mounted under — ``include_router(prefix=…)``
|
|
43
|
+
and/or the ASGI ``root_path`` — so templates can emit prefix-aware
|
|
44
|
+
absolute URLs (``/*`` links and ``fetch`` paths) instead of assuming the
|
|
45
|
+
service sits at the origin root.
|
|
46
|
+
|
|
47
|
+
The prefix is everything before the *last* occurrence of this route's first
|
|
48
|
+
logical segment (``/traces`` / ``/sessions``) in the request path. The last
|
|
49
|
+
occurrence — not the first — because the mount prefix may itself contain
|
|
50
|
+
that segment (``prefix="/api/traces"`` → ``/api/traces/traces``), while the
|
|
51
|
+
route's own segment is always the one nearest the path parameters.
|
|
52
|
+
"""
|
|
53
|
+
root_path = request.scope.get("root_path", "") or ""
|
|
54
|
+
marker = "/" + logical_path.strip("/").split("/")[0]
|
|
55
|
+
index = request.url.path.rfind(marker)
|
|
56
|
+
return root_path + (request.url.path[:index] if index > 0 else "")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _render_page(
|
|
60
|
+
request: Request, filename: str, logical_path: str, **replacements: str
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Read a UI template and fill ``__BASE__`` (mount prefix) plus any
|
|
63
|
+
caller placeholders (e.g. ``__RUN_ID__``)."""
|
|
64
|
+
text = (_TEMPLATES / filename).read_text(encoding="utf-8")
|
|
65
|
+
text = text.replace("__BASE__", _mount_base(request, logical_path))
|
|
66
|
+
for key, value in replacements.items():
|
|
67
|
+
text = text.replace(key, value)
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
41
71
|
class TagAssign(BaseModel):
|
|
42
72
|
tag: str = Field(min_length=1, max_length=64)
|
|
43
73
|
note: str = ""
|
|
@@ -93,8 +123,13 @@ def create_trace_router(
|
|
|
93
123
|
|
|
94
124
|
require_extra("tracing.web.create_trace_router", "fastapi", "web")
|
|
95
125
|
|
|
96
|
-
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
126
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
97
127
|
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
|
128
|
+
|
|
129
|
+
# `from __future__ import annotations` keeps every hint a string, resolved
|
|
130
|
+
# against this module's globals at decoration time; fastapi is imported
|
|
131
|
+
# lazily here, so publish the names the page handlers annotate with.
|
|
132
|
+
globals()["Request"] = Request
|
|
98
133
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
99
134
|
|
|
100
135
|
dependencies = []
|
|
@@ -286,26 +321,27 @@ def create_trace_router(
|
|
|
286
321
|
)
|
|
287
322
|
|
|
288
323
|
@router.get("/traces", response_class=HTMLResponse)
|
|
289
|
-
async def traces_list_page() -> str:
|
|
290
|
-
return (
|
|
324
|
+
async def traces_list_page(request: Request) -> str:
|
|
325
|
+
return _render_page(request, "ui.html", "/traces")
|
|
291
326
|
|
|
292
327
|
@router.get("/sessions", response_class=HTMLResponse)
|
|
293
|
-
async def sessions_list_page() -> str:
|
|
294
|
-
return (
|
|
328
|
+
async def sessions_list_page(request: Request) -> str:
|
|
329
|
+
return _render_page(request, "sessions.html", "/sessions")
|
|
295
330
|
|
|
296
331
|
@router.get("/traces/{trace_id}", response_class=HTMLResponse)
|
|
297
|
-
async def traces_run_page(trace_id: str) -> str:
|
|
332
|
+
async def traces_run_page(request: Request, trace_id: str) -> str:
|
|
298
333
|
from ..viz import trace_provenance_to_mermaid, trace_to_mermaid
|
|
299
334
|
|
|
300
335
|
trace = await store.get(trace_id)
|
|
301
336
|
mermaid = trace_to_mermaid(trace) if trace is not None else ""
|
|
302
337
|
provenance = trace_provenance_to_mermaid(trace) if trace is not None else ""
|
|
303
|
-
return (
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
338
|
+
return _render_page(
|
|
339
|
+
request,
|
|
340
|
+
"ui_run.html",
|
|
341
|
+
"/traces/{trace_id}",
|
|
342
|
+
__RUN_ID__=json.dumps(trace_id),
|
|
343
|
+
__MERMAID__=json.dumps(mermaid),
|
|
344
|
+
__MERMAID_GRAPH__=json.dumps(provenance),
|
|
309
345
|
)
|
|
310
346
|
|
|
311
347
|
return router
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
4
4
|
Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/bzdvdn/reactifact
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""`ChatAssistant(dispatcher=...)` — outbound side effects from a chat turn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from reactifact import PendingAction
|
|
9
|
+
from reactifact.agents import create_agent
|
|
10
|
+
from reactifact.chat import ChatAssistant
|
|
11
|
+
from reactifact.checkpoints import InMemoryKVBackend
|
|
12
|
+
from reactifact.consume import Consume
|
|
13
|
+
from reactifact.context import Context
|
|
14
|
+
from reactifact.produce import Produce, ProduceCall
|
|
15
|
+
from reactifact.session import SessionStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Msg(BaseModel):
|
|
19
|
+
text: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Reply(BaseModel):
|
|
23
|
+
text: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Notifier(Produce[_Reply]):
|
|
27
|
+
artifact_type = _Reply
|
|
28
|
+
also_creates = (PendingAction,)
|
|
29
|
+
|
|
30
|
+
async def produce(self, call: ProduceCall) -> None:
|
|
31
|
+
msg = call.trigger
|
|
32
|
+
if msg is None:
|
|
33
|
+
return
|
|
34
|
+
call.effects.act(
|
|
35
|
+
"notify", key=f"notify:{msg.data.text}", payload={"text": msg.data.text}
|
|
36
|
+
)
|
|
37
|
+
call.effects.create(_Reply(text=f"noted: {msg.data.text}"), id="reply:1")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _reply(ctx: Context, _message_id: str) -> dict[str, str]:
|
|
41
|
+
replies = ctx.list_artifacts(_Reply)
|
|
42
|
+
return {"reply": replies[-1].data.text if replies else ""}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_chat_assistant_runs_the_outbox_dispatcher():
|
|
46
|
+
sent: list[str] = []
|
|
47
|
+
|
|
48
|
+
async def dispatcher(context: Context, action) -> None:
|
|
49
|
+
sent.append(action.data.idempotency_key)
|
|
50
|
+
|
|
51
|
+
store = SessionStore(InMemoryKVBackend())
|
|
52
|
+
assistant = ChatAssistant(
|
|
53
|
+
store=store,
|
|
54
|
+
agents=[
|
|
55
|
+
create_agent("notify", consumes=[Consume(_Msg)], produces=[_Notifier()])
|
|
56
|
+
],
|
|
57
|
+
user_message=_Msg,
|
|
58
|
+
reply=_reply,
|
|
59
|
+
dispatcher=dispatcher,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = asyncio.run(assistant.invoke("hi", session_id="s1"))
|
|
63
|
+
|
|
64
|
+
assert result["reply"] == "noted: hi"
|
|
65
|
+
assert sent == ["notify:hi"]
|
|
66
|
+
|
|
67
|
+
session = asyncio.run(store.open("s1"))
|
|
68
|
+
action = session.context.get("action:notify:hi")
|
|
69
|
+
assert action is not None
|
|
70
|
+
assert action.data.status == "dispatched"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
from pydantic import BaseModel
|
|
5
6
|
from reactifact import Agent, Consume, Context, Patch, Runtime, RuntimeResources
|
|
@@ -868,3 +869,86 @@ def test_recording_llm_prompt_hash_defaults_empty():
|
|
|
868
869
|
llm = RecordingLLM(FakeLLM("x"), on_call=recorded.append, agent_of=lambda: "a")
|
|
869
870
|
asyncio.run(llm.complete(LLMRequest(messages=[Message.user("hi")])))
|
|
870
871
|
assert recorded[0].prompt_hash == ""
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def test_trace_pages_are_prefix_aware(tmp_path):
|
|
875
|
+
from fastapi import FastAPI
|
|
876
|
+
from fastapi.testclient import TestClient
|
|
877
|
+
from reactifact.tracing.web import create_trace_router
|
|
878
|
+
|
|
879
|
+
store = TraceStore(str(tmp_path / "prefix.db"))
|
|
880
|
+
run(store.export(RunTrace(id="r1", session_id="s", outcome="completed")))
|
|
881
|
+
app = FastAPI()
|
|
882
|
+
app.include_router(create_trace_router(store), prefix="/ai")
|
|
883
|
+
client = TestClient(app)
|
|
884
|
+
|
|
885
|
+
listing = client.get("/ai/traces")
|
|
886
|
+
assert listing.status_code == 200
|
|
887
|
+
html = listing.text
|
|
888
|
+
assert "__BASE__" not in html
|
|
889
|
+
assert 'href="/ai/traces/assets/app.css"' in html
|
|
890
|
+
assert 'href="/ai/traces"' in html
|
|
891
|
+
assert 'href="/ai/sessions"' in html
|
|
892
|
+
assert "'/ai/api/traces" in html
|
|
893
|
+
assert "'/ai/api/columns" in html
|
|
894
|
+
assert "'/ai/traces/'" in html
|
|
895
|
+
|
|
896
|
+
sessions = client.get("/ai/sessions").text
|
|
897
|
+
assert "__BASE__" not in sessions
|
|
898
|
+
assert "'/ai/api/sessions" in sessions
|
|
899
|
+
assert 'href="/ai/traces"' in sessions
|
|
900
|
+
|
|
901
|
+
run_page = client.get("/ai/traces/r1").text
|
|
902
|
+
assert "__BASE__" not in run_page
|
|
903
|
+
assert "'/ai/api/traces/" in run_page
|
|
904
|
+
assert 'href="/ai/traces/assets/app.css"' in run_page
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def test_trace_pages_without_prefix_keep_root_paths(tmp_path):
|
|
908
|
+
from fastapi import FastAPI
|
|
909
|
+
from fastapi.testclient import TestClient
|
|
910
|
+
from reactifact.tracing.web import create_trace_router
|
|
911
|
+
|
|
912
|
+
store = TraceStore(str(tmp_path / "root.db"))
|
|
913
|
+
app = FastAPI()
|
|
914
|
+
app.include_router(create_trace_router(store))
|
|
915
|
+
client = TestClient(app)
|
|
916
|
+
html = client.get("/traces").text
|
|
917
|
+
assert "__BASE__" not in html
|
|
918
|
+
assert 'href="/traces/assets/app.css"' in html
|
|
919
|
+
assert "'/api/traces" in html
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def test_trace_pages_honor_asgi_root_path(tmp_path):
|
|
923
|
+
from fastapi import FastAPI
|
|
924
|
+
from fastapi.testclient import TestClient
|
|
925
|
+
from reactifact.tracing.web import create_trace_router
|
|
926
|
+
|
|
927
|
+
store = TraceStore(str(tmp_path / "rootpath.db"))
|
|
928
|
+
app = FastAPI(root_path="/ai")
|
|
929
|
+
app.include_router(create_trace_router(store))
|
|
930
|
+
client = TestClient(app)
|
|
931
|
+
html = client.get("/traces").text
|
|
932
|
+
assert "__BASE__" not in html
|
|
933
|
+
assert 'href="/ai/traces/assets/app.css"' in html
|
|
934
|
+
assert "'/ai/api/traces" in html
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def test_trace_pages_stay_prefixed_when_prefix_contains_a_route_segment(tmp_path):
|
|
938
|
+
"""A mount prefix that itself contains ``/traces`` (or ``/sessions``) must
|
|
939
|
+
not confuse the base: every absolute ref stays inside the prefix."""
|
|
940
|
+
from fastapi import FastAPI
|
|
941
|
+
from fastapi.testclient import TestClient
|
|
942
|
+
from reactifact.tracing.web import create_trace_router
|
|
943
|
+
|
|
944
|
+
store = TraceStore(str(tmp_path / "tricky.db"))
|
|
945
|
+
run(store.export(RunTrace(id="r1", session_id="s", outcome="completed")))
|
|
946
|
+
for prefix in ("/api/traces", "/traces-app", "/sessions", "/traces"):
|
|
947
|
+
app = FastAPI()
|
|
948
|
+
app.include_router(create_trace_router(store), prefix=prefix)
|
|
949
|
+
client = TestClient(app)
|
|
950
|
+
for logical in ("/traces", "/sessions", "/traces/r1"):
|
|
951
|
+
html = client.get(prefix + logical).text
|
|
952
|
+
assert "__BASE__" not in html
|
|
953
|
+
refs = re.findall(r'(?:href="|fetch\(\')(/[^"\'?]*)', html)
|
|
954
|
+
assert all(u.startswith(prefix) for u in refs), (prefix, logical, refs[:5])
|
|
File without changes
|
|
File without changes
|