@polymorphism-tech/morph-spec 8.38.0 → 8.41.0
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.
- package/CLAUDE.md +20 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +59 -25
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -3,128 +3,55 @@ using System.Text.Json.Serialization;
|
|
|
3
3
|
|
|
4
4
|
namespace Morph.AiKit.Evals;
|
|
5
5
|
|
|
6
|
-
/// <summary>Estado da arvore de cache nesta execucao, como o sumario o reporta.</summary>
|
|
7
6
|
public static class EvalCacheState
|
|
8
7
|
{
|
|
9
|
-
/// <summary>Todos os cenarios resolveram pela cache. Nenhuma chamada ao provider.</summary>
|
|
10
8
|
public const string Warm = "warm";
|
|
11
9
|
|
|
12
|
-
/// <summary>Ao menos um cenario foi ao provider — a arvore mudou e precisa ser commitada.</summary>
|
|
13
10
|
public const string Cold = "cold";
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
/// <summary>Uma falha de eval, com nome — cenario, assercao e o motivo literal.</summary>
|
|
17
|
-
/// <remarks>
|
|
18
|
-
/// <para>
|
|
19
|
-
/// <b>Por que o sumario carrega isto, e nao so contadores.</b> Medido: degradar a resposta em
|
|
20
|
-
/// cache deixava a suite vermelha (correto) com a mensagem <c>Expected 0, Actual 1</c> —
|
|
21
|
-
/// verdadeira e inutil. Nada a jusante conseguia dizer QUAL cenario caiu nem por que, porque a
|
|
22
|
-
/// informacao morria dentro do fixture.
|
|
23
|
-
/// </para>
|
|
24
|
-
/// <para>
|
|
25
|
-
/// A rubrica <c>eval-de-modelo.md</c> exige, na banda 9-10, "derrubando o cenario e
|
|
26
|
-
/// NOMEANDO-O", e da como exemplo <c>eval-fail: triagem.acme.a-saudacao</c>. Sem este tipo a
|
|
27
|
-
/// propria rubrica seria inexequivel — o que e pior do que nao ter rubrica, porque parece
|
|
28
|
-
/// coberto.
|
|
29
|
-
/// </para>
|
|
30
|
-
/// </remarks>
|
|
31
|
-
/// <param name="Scenario">A chave do cenario (<c>{agente}.{tenant}.{nome}</c>).</param>
|
|
32
|
-
/// <param name="Assertion">O nome da assercao que reprovou.</param>
|
|
33
|
-
/// <param name="Kind">A regua usada, para distinguir duas assercoes homonimas de tipos distintos.</param>
|
|
34
|
-
/// <param name="Reason">O motivo literal produzido pela assercao.</param>
|
|
35
13
|
public sealed record EvalFailureDetail(
|
|
36
14
|
[property: JsonPropertyName("scenario")] string Scenario,
|
|
37
15
|
[property: JsonPropertyName("assertion")] string Assertion,
|
|
38
16
|
[property: JsonPropertyName("kind")] string Kind,
|
|
39
17
|
[property: JsonPropertyName("reason")] string Reason)
|
|
40
18
|
{
|
|
41
|
-
/// <summary>Uma linha legivel, no formato que a rubrica cita.</summary>
|
|
42
19
|
public override string ToString()
|
|
43
20
|
=> $"eval-fail: {Scenario} — {Assertion} ({Kind}): {Reason}";
|
|
44
21
|
}
|
|
45
22
|
|
|
46
|
-
/// <summary>
|
|
47
|
-
/// O sumario que o fixture escreve no fim da suite, e que o CLI le.
|
|
48
|
-
/// </summary>
|
|
49
|
-
/// <remarks>
|
|
50
|
-
/// <para>
|
|
51
|
-
/// <b>Por que ARQUIVO e nao uma linha marcada no stdout.</b> O runner de teste captura a
|
|
52
|
-
/// saida de console por teste, e o que chega ao stdout do processo depende da configuracao
|
|
53
|
-
/// do runner e do logger. Um contrato que quebra quando alguem liga um logger e um contrato
|
|
54
|
-
/// ruim — e a falha seria SILENCIOSA (sumario ausente, contagem desconhecida, no verde pelo
|
|
55
|
-
/// codigo de saida). O arquivo tem outra virtude: quem escolhe o caminho e o CLI, entao dois
|
|
56
|
-
/// nos de eval no mesmo <c>verify</c> nao colidem.
|
|
57
|
-
/// </para>
|
|
58
|
-
/// <para>
|
|
59
|
-
/// <b>Do outro lado, o leitor trata sumario ausente ou ilegivel como DESCONHECIDO, jamais
|
|
60
|
-
/// como zero.</b> Um sumario que nao parseia nao pode fabricar <c>scenarios: 0</c> e
|
|
61
|
-
/// transformar uma baseline em queda inventada.
|
|
62
|
-
/// </para>
|
|
63
|
-
/// </remarks>
|
|
64
23
|
public sealed record EvalRunSummary
|
|
65
24
|
{
|
|
66
|
-
/// <summary>Quantos cenarios foram avaliados.</summary>
|
|
67
25
|
[JsonPropertyName("scenarios")]
|
|
68
26
|
public required int Scenarios { get; init; }
|
|
69
27
|
|
|
70
|
-
/// <summary>Quantos passaram em TODAS as suas assercoes deterministicas.</summary>
|
|
71
28
|
[JsonPropertyName("passed")]
|
|
72
29
|
public required int Passed { get; init; }
|
|
73
30
|
|
|
74
|
-
/// <summary>Quantos falharam em ao menos uma assercao deterministica.</summary>
|
|
75
31
|
[JsonPropertyName("failed")]
|
|
76
32
|
public required int Failed { get; init; }
|
|
77
33
|
|
|
78
|
-
/// <summary><see cref="EvalCacheState.Warm"/> ou <see cref="EvalCacheState.Cold"/>.</summary>
|
|
79
34
|
[JsonPropertyName("cache")]
|
|
80
35
|
public required string Cache { get; init; }
|
|
81
36
|
|
|
82
|
-
/// <summary>Veredito do carimbo, em minusculas (<c>ok</c>, <c>missing</c>, ...).</summary>
|
|
83
37
|
[JsonPropertyName("stamp")]
|
|
84
38
|
public required string Stamp { get; init; }
|
|
85
39
|
|
|
86
|
-
/// <summary>
|
|
87
|
-
/// Quantas vezes a cache falhou e a chamada desceu ao cliente interno. Em modo offline
|
|
88
|
-
/// com cache quente e ZERO — e esse zero e a prova de que nenhuma chamada paga foi feita.
|
|
89
|
-
/// </summary>
|
|
90
40
|
[JsonPropertyName("misses")]
|
|
91
41
|
public required int Misses { get; init; }
|
|
92
42
|
|
|
93
|
-
/// <summary>O <c>ProviderName</c> usado, o mesmo que entrou na chave.</summary>
|
|
94
43
|
[JsonPropertyName("provider")]
|
|
95
44
|
public required string Provider { get; init; }
|
|
96
45
|
|
|
97
|
-
/// <summary>O <c>DefaultModelId</c> usado, o mesmo que entrou na chave.</summary>
|
|
98
46
|
[JsonPropertyName("model")]
|
|
99
47
|
public string? Model { get; init; }
|
|
100
48
|
|
|
101
|
-
/// <summary>
|
|
102
|
-
/// Metricas de JUIZO (avaliadores de qualidade), nome para valor. Reportadas, nunca
|
|
103
|
-
/// decisivas: com a cache quente elas sao deterministicas, mas a variancia volta na
|
|
104
|
-
/// regravacao, e um gate que dependesse disso nasceria destinado a ser desligado.
|
|
105
|
-
/// </summary>
|
|
106
49
|
[JsonPropertyName("quality")]
|
|
107
50
|
public IReadOnlyDictionary<string, double> Quality { get; init; } = new Dictionary<string, double>(StringComparer.Ordinal);
|
|
108
51
|
|
|
109
|
-
/// <summary>
|
|
110
|
-
/// As falhas, uma por assercao reprovada, com nome. Vazia num run verde.
|
|
111
|
-
/// </summary>
|
|
112
|
-
/// <remarks>
|
|
113
|
-
/// Lista, e nao um booleano ou um contador, porque um cenario pode quebrar em mais de uma
|
|
114
|
-
/// assercao e as duas informam coisas diferentes: uma resposta vazia derruba
|
|
115
|
-
/// <c>Contains</c> E <c>JsonHasKey</c>, e saber disso e a diferenca entre "o prompt mudou"
|
|
116
|
-
/// e "o modelo parou de responder".
|
|
117
|
-
/// </remarks>
|
|
118
52
|
[JsonPropertyName("failures")]
|
|
119
53
|
public IReadOnlyList<EvalFailureDetail> Failures { get; init; } = [];
|
|
120
54
|
|
|
121
|
-
/// <summary>
|
|
122
|
-
/// As falhas em texto, uma por linha — a mensagem que um teste ou o CLI mostra.
|
|
123
|
-
/// </summary>
|
|
124
|
-
/// <remarks>
|
|
125
|
-
/// Devolve frase propria quando nao ha falha alguma, em vez de string vazia: uma mensagem
|
|
126
|
-
/// de assercao vazia e indistinguivel de uma mensagem que ninguem preencheu.
|
|
127
|
-
/// </remarks>
|
|
128
55
|
public string DescribeFailures()
|
|
129
56
|
=> Failures.Count == 0
|
|
130
57
|
? "nenhuma falha de eval registrada"
|
|
@@ -132,7 +59,6 @@ public sealed record EvalRunSummary
|
|
|
132
59
|
|
|
133
60
|
private static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
|
|
134
61
|
|
|
135
|
-
/// <summary>Grava o sumario. Cria o diretorio se preciso; sobrescreve se ja existe.</summary>
|
|
136
62
|
public void WriteTo(string path)
|
|
137
63
|
{
|
|
138
64
|
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
|
@@ -146,7 +72,6 @@ public sealed record EvalRunSummary
|
|
|
146
72
|
File.WriteAllText(path, JsonSerializer.Serialize(this, Json));
|
|
147
73
|
}
|
|
148
74
|
|
|
149
|
-
/// <summary>Le um sumario gravado. Devolve nulo para arquivo ausente ou ilegivel — DESCONHECIDO, jamais zero.</summary>
|
|
150
75
|
public static EvalRunSummary? ReadFrom(string path)
|
|
151
76
|
{
|
|
152
77
|
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
|
@@ -2,39 +2,6 @@ using Microsoft.Extensions.AI;
|
|
|
2
2
|
|
|
3
3
|
namespace Morph.AiKit.Evals;
|
|
4
4
|
|
|
5
|
-
/// <summary>
|
|
6
|
-
/// O <see cref="IChatClient"/> que RECUSA no miss: a peca que torna possivel rodar evals no
|
|
7
|
-
/// CI sem uma unica credencial.
|
|
8
|
-
/// </summary>
|
|
9
|
-
/// <remarks>
|
|
10
|
-
/// <para>
|
|
11
|
-
/// <b>Onde ele fica.</b> A biblioteca de Evaluation embrulha o cliente que recebe num
|
|
12
|
-
/// cliente de cache. No HIT, o cliente interno nunca e invocado. No MISS, a chamada desce
|
|
13
|
-
/// para o interno — e o interno, aqui, e este. Logo: com a cache quente ele nao e chamado
|
|
14
|
-
/// nenhuma vez, e quando e chamado a resposta certa e "faltou cenario na cache", nao uma
|
|
15
|
-
/// chamada de rede.
|
|
16
|
-
/// </para>
|
|
17
|
-
/// <para>
|
|
18
|
-
/// <b>Por que ele replica <c>ProviderName</c> e <c>DefaultModelId</c>.</b> Esses dois
|
|
19
|
-
/// campos do <see cref="ChatClientMetadata"/> entram na chave de cache. Se o cliente
|
|
20
|
-
/// offline se apresentasse com outro nome, a chave computada na leitura seria outra e TODO
|
|
21
|
-
/// cenario daria miss — uma cache quente indistinguivel de uma cache vazia. Eles sao
|
|
22
|
-
/// parametros, e nao constantes, exatamente porque tem de reproduzir o run que GRAVOU.
|
|
23
|
-
/// </para>
|
|
24
|
-
/// <para>
|
|
25
|
-
/// <b><see cref="ChatClientMetadata.ProviderUri"/> e deliberadamente nulo.</b> O endpoint
|
|
26
|
-
/// NAO entra na chave — medido contra a v10.9.0, contra o que a documentacao afirma. E essa
|
|
27
|
-
/// ausencia que permite ao cliente offline nao ter endpoint nenhum: ele nao tem para onde
|
|
28
|
-
/// ligar, e nao precisa ter.
|
|
29
|
-
/// </para>
|
|
30
|
-
/// <para>
|
|
31
|
-
/// <b><see cref="Invocations"/> e a prova, e ela e um contador, nao um sniffer.</b> "Zero
|
|
32
|
-
/// chamadas de rede" nao se mede sem instrumentar a rede; "zero invocacoes do cliente
|
|
33
|
-
/// interno" se mede aqui, e e suficiente porque o cliente interno e o UNICO caminho para a
|
|
34
|
-
/// rede nesta montagem. O contador sobe ANTES do lancamento, para que uma tentativa
|
|
35
|
-
/// interrompida ainda apareca.
|
|
36
|
-
/// </para>
|
|
37
|
-
/// </remarks>
|
|
38
5
|
public sealed class OfflineChatClient : IChatClient
|
|
39
6
|
{
|
|
40
7
|
private readonly Lock _gate = new();
|
|
@@ -43,18 +10,7 @@ public sealed class OfflineChatClient : IChatClient
|
|
|
43
10
|
private readonly string _envFile;
|
|
44
11
|
private int _invocations;
|
|
45
12
|
|
|
46
|
-
|
|
47
|
-
/// Cria o cliente offline replicando a identidade do run que gravou a cache.
|
|
48
|
-
/// </summary>
|
|
49
|
-
/// <param name="providerName">Mesmo <c>ProviderName</c> do run que gravou. Entra na chave.</param>
|
|
50
|
-
/// <param name="defaultModelId">Mesmo <c>DefaultModelId</c> do run que gravou. Entra na chave.</param>
|
|
51
|
-
/// <param name="cachePath">Raiz da arvore de cache, so para a mensagem de miss.</param>
|
|
52
|
-
/// <param name="envFile">Arquivo de credencial esperado, so para a mensagem de miss.</param>
|
|
53
|
-
/// <param name="currentScenario">
|
|
54
|
-
/// Le o nome do cenario em execucao no momento da chamada. E uma funcao, e nao um
|
|
55
|
-
/// valor, porque um fixture roda N cenarios com o MESMO cliente: um nome capturado na
|
|
56
|
-
/// construcao nomearia sempre o primeiro.
|
|
57
|
-
/// </param>
|
|
13
|
+
// ProviderName e DefaultModelId entram na chave de cache: tem de reproduzir o run que gravou.
|
|
58
14
|
public OfflineChatClient(
|
|
59
15
|
string providerName,
|
|
60
16
|
string? defaultModelId,
|
|
@@ -74,36 +30,27 @@ public sealed class OfflineChatClient : IChatClient
|
|
|
74
30
|
_currentScenario = currentScenario;
|
|
75
31
|
}
|
|
76
32
|
|
|
77
|
-
/// <summary>O <c>ProviderName</c> replicado — o mesmo que gravou a cache.</summary>
|
|
78
33
|
public string ProviderName { get; }
|
|
79
34
|
|
|
80
|
-
/// <summary>O <c>DefaultModelId</c> replicado — o mesmo que gravou a cache.</summary>
|
|
81
35
|
public string? DefaultModelId { get; }
|
|
82
36
|
|
|
83
|
-
/// <summary>
|
|
84
|
-
/// Quantas vezes a cache falhou e a chamada desceu ate aqui. Num run com a cache quente
|
|
85
|
-
/// e ZERO, e essa e a prova de que nenhuma chamada paga foi feita.
|
|
86
|
-
/// </summary>
|
|
87
37
|
public int Invocations
|
|
88
38
|
{
|
|
89
39
|
get { lock (_gate) { return _invocations; } }
|
|
90
40
|
}
|
|
91
41
|
|
|
92
|
-
/// <inheritdoc />
|
|
93
42
|
public Task<ChatResponse> GetResponseAsync(
|
|
94
43
|
IEnumerable<ChatMessage> messages,
|
|
95
44
|
ChatOptions? options = null,
|
|
96
45
|
CancellationToken cancellationToken = default)
|
|
97
46
|
=> throw Miss();
|
|
98
47
|
|
|
99
|
-
/// <inheritdoc />
|
|
100
48
|
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
|
101
49
|
IEnumerable<ChatMessage> messages,
|
|
102
50
|
ChatOptions? options = null,
|
|
103
51
|
CancellationToken cancellationToken = default)
|
|
104
52
|
=> throw Miss();
|
|
105
53
|
|
|
106
|
-
/// <inheritdoc />
|
|
107
54
|
public object? GetService(Type serviceType, object? serviceKey = null)
|
|
108
55
|
{
|
|
109
56
|
ArgumentNullException.ThrowIfNull(serviceType);
|
|
@@ -115,19 +62,15 @@ public sealed class OfflineChatClient : IChatClient
|
|
|
115
62
|
|
|
116
63
|
if (serviceType == typeof(ChatClientMetadata))
|
|
117
64
|
{
|
|
118
|
-
//
|
|
119
|
-
// cliente que nunca liga para lugar nenhum nao deve anunciar um destino.
|
|
65
|
+
// O endpoint nao entra na chave de cache (medido na 10.9.0), entao o cliente offline nao anuncia nenhum.
|
|
120
66
|
return new ChatClientMetadata(ProviderName, providerUri: null, DefaultModelId);
|
|
121
67
|
}
|
|
122
68
|
|
|
123
69
|
return serviceType.IsInstanceOfType(this) ? this : null;
|
|
124
70
|
}
|
|
125
71
|
|
|
126
|
-
/// <inheritdoc />
|
|
127
72
|
public void Dispose()
|
|
128
73
|
{
|
|
129
|
-
// Nada a liberar: este cliente nao abre socket, nao le ambiente e nao guarda estado
|
|
130
|
-
// fora do contador. E o ponto.
|
|
131
74
|
}
|
|
132
75
|
|
|
133
76
|
private EvalCacheMissException Miss()
|
|
@@ -7,79 +7,17 @@ using Microsoft.Extensions.Caching.Distributed;
|
|
|
7
7
|
|
|
8
8
|
namespace Morph.AiKit.Evals;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
/// A cache de respostas em disco do kit, com caminhos CURTOS o bastante para serem
|
|
12
|
-
/// commitados e instalados no Windows.
|
|
13
|
-
/// </summary>
|
|
14
|
-
/// <remarks>
|
|
15
|
-
/// <para>
|
|
16
|
-
/// <b>Por que não usamos o <c>DiskBasedResponseCacheProvider</c> da biblioteca.</b> Ele nomeia
|
|
17
|
-
/// o diretório de cada entrada com o hash inteiro em hexadecimal — <b>96 caracteres</b>. Somado
|
|
18
|
-
/// ao resto (<c>cache/{cenário}/{iteração}/</c>, mais o nome do arquivo), o caminho relativo
|
|
19
|
-
/// chega a <b>225</b> caracteres; com a raiz deste repositório (73) o absoluto dá <b>298</b>,
|
|
20
|
-
/// contra o teto de <b>260</b> do <c>MAX_PATH</c> do Windows.
|
|
21
|
-
/// </para>
|
|
22
|
-
/// <para>
|
|
23
|
-
/// <b>O sintoma foi pior que o erro.</b> O git não recusou barulhento: ele avisou
|
|
24
|
-
/// <c>could not open directory ... Filename too long</c> e commitou <b>1 de 9</b> arquivos — só
|
|
25
|
-
/// o carimbo. A árvore de cache "commitada" não existia no commit, e o canário de CI
|
|
26
|
-
/// (<c>git diff --exit-code</c>) reportava LIMPO porque o git nunca enxergou os arquivos.
|
|
27
|
-
/// Um guarda que não consegue observar o objeto reporta verde por construção.
|
|
28
|
-
/// </para>
|
|
29
|
-
/// <para>
|
|
30
|
-
/// <b>O que muda, e só isso:</b> o segmento de hash passa a ser um PREFIXO de
|
|
31
|
-
/// <see cref="KeyLength"/> caracteres do mesmo hash. O resto do layout é o da biblioteca —
|
|
32
|
-
/// <c>cache/{cenário}/{iteração}/{chave}/{entry.json,contents.data}</c>.
|
|
33
|
-
/// </para>
|
|
34
|
-
/// <para>
|
|
35
|
-
/// <b>Truncar é seguro AQUI por construção, e não por contagem de bits.</b> O
|
|
36
|
-
/// <c>entry.json</c> guarda a chave INTEIRA, e a leitura compara: prefixo que colide com outra
|
|
37
|
-
/// chave devolve <b>miss</b>, nunca a resposta errada. Um miss é caro (regravar) e visível
|
|
38
|
-
/// (o modo offline reprova nomeando o cenário); uma resposta trocada seria um eval verde sobre
|
|
39
|
-
/// a resposta de outro cenário — a única falha que este kit não pode ter.
|
|
40
|
-
/// </para>
|
|
41
|
-
/// <para>
|
|
42
|
-
/// <b>Expiração NÃO é destrutiva.</b> A implementação da biblioteca apaga o diretório da chave
|
|
43
|
-
/// ao ler uma entrada vencida — numa árvore commitada isso significa `git status` sujo e miss
|
|
44
|
-
/// em massa num dia em que ninguém mexeu em nada. Aqui, entrada vencida devolve <c>null</c> e
|
|
45
|
-
/// não toca em disco. O piso de TTL de um ano continua valendo como segunda linha de defesa:
|
|
46
|
-
/// duas proteções para o mesmo tiro é barato quando o tiro apaga trabalho commitado.
|
|
47
|
-
/// </para>
|
|
48
|
-
/// <para>
|
|
49
|
-
/// <b><see cref="ShortPathResponseCache.Refresh"/> é um no-op deliberado.</b> A validade é
|
|
50
|
-
/// absoluta a partir da gravação, e não deslizante. Se <c>Refresh</c> reescrevesse o
|
|
51
|
-
/// <c>entry.json</c>, uma LEITURA sujaria a árvore — e o invariante "um run offline não escreve
|
|
52
|
-
/// um byte" cairia, junto com o canário que o cobra.
|
|
53
|
-
/// </para>
|
|
54
|
-
/// </remarks>
|
|
10
|
+
// O DiskBasedResponseCacheProvider da biblioteca nomeia a entrada com o hash inteiro (96 chars) e estoura o MAX_PATH do Windows.
|
|
55
11
|
public sealed class ShortPathResponseCacheProvider : IEvaluationResponseCacheProvider
|
|
56
12
|
{
|
|
57
|
-
/// <summary>
|
|
58
|
-
/// Quantos caracteres do hash nomeiam o diretório da entrada.
|
|
59
|
-
/// </summary>
|
|
60
|
-
/// <remarks>
|
|
61
|
-
/// 16 hexadecimais = 64 bits. O número não precisa ser à prova de colisão sozinho: a
|
|
62
|
-
/// verificação da chave inteira no <c>entry.json</c> transforma qualquer colisão num miss.
|
|
63
|
-
/// Ele precisa apenas ser curto — e 16 derruba o pior caminho relativo de 225 para 145 (medido).
|
|
64
|
-
/// </remarks>
|
|
65
13
|
public const int KeyLength = 16;
|
|
66
14
|
|
|
67
|
-
/// <summary>Subdiretório que separa as entradas do carimbo, na raiz da árvore.</summary>
|
|
68
|
-
/// <remarks>
|
|
69
|
-
/// Mantido (a biblioteca também o usa) porque é ele que torna <see cref="ResetAsync"/>
|
|
70
|
-
/// trivialmente seguro: zerar a cache é apagar UM diretório, sem risco de levar o carimbo
|
|
71
|
-
/// junto. Custa seis caracteres e remove uma classe inteira de engano.
|
|
72
|
-
/// </remarks>
|
|
73
15
|
public const string EntriesDirectory = "cache";
|
|
74
16
|
|
|
75
17
|
private readonly string _root;
|
|
76
18
|
private readonly TimeSpan _timeToLive;
|
|
77
19
|
private readonly TimeProvider _clock;
|
|
78
20
|
|
|
79
|
-
/// <summary>Cria o provedor sobre uma raiz de cache.</summary>
|
|
80
|
-
/// <param name="storageRootPath">Raiz da árvore (a mesma que carrega o carimbo).</param>
|
|
81
|
-
/// <param name="timeToLive">Validade absoluta das entradas, contada da gravação.</param>
|
|
82
|
-
/// <param name="clock">Relógio injetado — a expiração é uma leitura de relógio.</param>
|
|
83
21
|
public ShortPathResponseCacheProvider(string storageRootPath, TimeSpan timeToLive, TimeProvider? clock = null)
|
|
84
22
|
{
|
|
85
23
|
ArgumentException.ThrowIfNullOrWhiteSpace(storageRootPath);
|
|
@@ -89,10 +27,8 @@ public sealed class ShortPathResponseCacheProvider : IEvaluationResponseCachePro
|
|
|
89
27
|
_clock = clock ?? TimeProvider.System;
|
|
90
28
|
}
|
|
91
29
|
|
|
92
|
-
/// <summary>Onde as entradas vivem, dentro da raiz.</summary>
|
|
93
30
|
public string EntriesRoot => Path.Combine(_root, EntriesDirectory);
|
|
94
31
|
|
|
95
|
-
/// <inheritdoc />
|
|
96
32
|
public ValueTask<IDistributedCache> GetCacheAsync(
|
|
97
33
|
string scenarioName,
|
|
98
34
|
string iterationName,
|
|
@@ -108,8 +44,6 @@ public sealed class ShortPathResponseCacheProvider : IEvaluationResponseCachePro
|
|
|
108
44
|
return ValueTask.FromResult(cache);
|
|
109
45
|
}
|
|
110
46
|
|
|
111
|
-
/// <inheritdoc />
|
|
112
|
-
/// <remarks>Apaga TODAS as entradas e preserva o carimbo. Raiz ausente é sucesso: não há o que zerar.</remarks>
|
|
113
47
|
public ValueTask ResetAsync(CancellationToken cancellationToken = default)
|
|
114
48
|
{
|
|
115
49
|
cancellationToken.ThrowIfCancellationRequested();
|
|
@@ -122,11 +56,6 @@ public sealed class ShortPathResponseCacheProvider : IEvaluationResponseCachePro
|
|
|
122
56
|
return ValueTask.CompletedTask;
|
|
123
57
|
}
|
|
124
58
|
|
|
125
|
-
/// <inheritdoc />
|
|
126
|
-
/// <remarks>
|
|
127
|
-
/// Este é o ÚNICO caminho que apaga entrada vencida, e ele só roda quando alguém o chama de
|
|
128
|
-
/// propósito. A leitura nunca apaga — ver as observações do tipo.
|
|
129
|
-
/// </remarks>
|
|
130
59
|
public ValueTask DeleteExpiredCacheEntriesAsync(CancellationToken cancellationToken = default)
|
|
131
60
|
{
|
|
132
61
|
cancellationToken.ThrowIfCancellationRequested();
|
|
@@ -150,16 +79,10 @@ public sealed class ShortPathResponseCacheProvider : IEvaluationResponseCachePro
|
|
|
150
79
|
}
|
|
151
80
|
}
|
|
152
81
|
|
|
153
|
-
/// <summary>
|
|
154
|
-
/// A cache de UM par (cenário, iteração). Ver <see cref="ShortPathResponseCacheProvider"/> para
|
|
155
|
-
/// o porquê de ela existir.
|
|
156
|
-
/// </summary>
|
|
157
82
|
internal sealed class ShortPathResponseCache : IDistributedCache
|
|
158
83
|
{
|
|
159
|
-
/// <summary>Metadados da entrada. Nome idêntico ao da biblioteca — o layout é o mesmo.</summary>
|
|
160
84
|
internal const string EntryFileName = "entry.json";
|
|
161
85
|
|
|
162
|
-
/// <summary>Payload cru da resposta. Nome idêntico ao da biblioteca.</summary>
|
|
163
86
|
internal const string ContentsFileName = "contents.data";
|
|
164
87
|
|
|
165
88
|
private static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
|
|
@@ -180,16 +103,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
180
103
|
_clock = clock;
|
|
181
104
|
}
|
|
182
105
|
|
|
183
|
-
/// <summary>
|
|
184
|
-
/// O prefixo do hash que nomeia o diretório da entrada.
|
|
185
|
-
/// </summary>
|
|
186
|
-
/// <remarks>
|
|
187
|
-
/// A chave que a biblioteca entrega já é um hash hexadecimal; pegar o prefixo dela mantém o
|
|
188
|
-
/// nome CORRELACIONÁVEL com a chave real (é literalmente o começo dela), o que uma segunda
|
|
189
|
-
/// função de hash destruiria. Uma chave que não seja hexadecimal — improvável, mas não
|
|
190
|
-
/// impossível se a biblioteca mudar — é reduzida por SHA-256 antes de truncar, para que o
|
|
191
|
-
/// nome continue sendo um segmento de caminho válido em qualquer sistema de arquivos.
|
|
192
|
-
/// </remarks>
|
|
193
106
|
internal static string ShortKey(string key)
|
|
194
107
|
{
|
|
195
108
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
|
@@ -203,7 +116,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
203
116
|
: usable[..ShortPathResponseCacheProvider.KeyLength];
|
|
204
117
|
}
|
|
205
118
|
|
|
206
|
-
/// <inheritdoc />
|
|
207
119
|
public byte[]? Get(string key)
|
|
208
120
|
{
|
|
209
121
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
|
@@ -218,16 +130,13 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
218
130
|
return null;
|
|
219
131
|
}
|
|
220
132
|
|
|
221
|
-
//
|
|
222
|
-
// sem esta comparação, a colisão devolveria a resposta de OUTRO cenário — a única falha
|
|
223
|
-
// que um eval não pode ter. Com ela, colisão vira miss, que é caro e visível.
|
|
133
|
+
// O diretorio e so um prefixo do hash: a chave inteira decide, e colisao vira miss em vez da resposta de outro cenario.
|
|
224
134
|
if (!string.Equals(entry.Key, key, StringComparison.Ordinal))
|
|
225
135
|
{
|
|
226
136
|
return null;
|
|
227
137
|
}
|
|
228
138
|
|
|
229
|
-
// Vencida:
|
|
230
|
-
// árvore commitada sumir sozinha e sujar o git status de quem só rodou os testes.
|
|
139
|
+
// Vencida nao apaga, ao contrario da biblioteca: a arvore e commitada e uma leitura nao pode sujar o git status.
|
|
231
140
|
if (entry.Expiration <= _clock.GetUtcNow())
|
|
232
141
|
{
|
|
233
142
|
return null;
|
|
@@ -236,14 +145,12 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
236
145
|
return File.ReadAllBytes(contentsPath);
|
|
237
146
|
}
|
|
238
147
|
|
|
239
|
-
/// <inheritdoc />
|
|
240
148
|
public Task<byte[]?> GetAsync(string key, CancellationToken token = default)
|
|
241
149
|
{
|
|
242
150
|
token.ThrowIfCancellationRequested();
|
|
243
151
|
return Task.FromResult(Get(key));
|
|
244
152
|
}
|
|
245
153
|
|
|
246
|
-
/// <inheritdoc />
|
|
247
154
|
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
|
248
155
|
{
|
|
249
156
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
|
@@ -263,14 +170,11 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
263
170
|
};
|
|
264
171
|
|
|
265
172
|
File.WriteAllBytes(Path.Combine(dir, ContentsFileName), value);
|
|
266
|
-
// Quebras normalizadas para LF e uma quebra final: estes arquivos são COMMITADOS, e
|
|
267
|
-
// CRLF numa máquina com LF noutra é diff que não é diff.
|
|
268
173
|
File.WriteAllText(
|
|
269
174
|
Path.Combine(dir, EntryFileName),
|
|
270
175
|
JsonSerializer.Serialize(entry, Json).ReplaceLineEndings("\n") + "\n");
|
|
271
176
|
}
|
|
272
177
|
|
|
273
|
-
/// <inheritdoc />
|
|
274
178
|
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
|
275
179
|
{
|
|
276
180
|
token.ThrowIfCancellationRequested();
|
|
@@ -278,18 +182,11 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
278
182
|
return Task.CompletedTask;
|
|
279
183
|
}
|
|
280
184
|
|
|
281
|
-
|
|
282
|
-
/// <remarks>
|
|
283
|
-
/// NO-OP deliberado. A validade é absoluta a partir da gravação, nunca deslizante: se este
|
|
284
|
-
/// método reescrevesse o <c>entry.json</c>, uma LEITURA passaria a escrever, e o invariante
|
|
285
|
-
/// "um run offline não muda um byte da árvore" — com o canário que o cobra — cairia junto.
|
|
286
|
-
/// </remarks>
|
|
185
|
+
// No-op: a validade e absoluta, e reescrever o entry.json faria uma leitura escrever na arvore commitada.
|
|
287
186
|
public void Refresh(string key) { }
|
|
288
187
|
|
|
289
|
-
/// <inheritdoc />
|
|
290
188
|
public Task RefreshAsync(string key, CancellationToken token = default) => Task.CompletedTask;
|
|
291
189
|
|
|
292
|
-
/// <inheritdoc />
|
|
293
190
|
public void Remove(string key)
|
|
294
191
|
{
|
|
295
192
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
|
@@ -301,7 +198,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
301
198
|
}
|
|
302
199
|
}
|
|
303
200
|
|
|
304
|
-
/// <inheritdoc />
|
|
305
201
|
public Task RemoveAsync(string key, CancellationToken token = default)
|
|
306
202
|
{
|
|
307
203
|
token.ThrowIfCancellationRequested();
|
|
@@ -309,7 +205,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
309
205
|
return Task.CompletedTask;
|
|
310
206
|
}
|
|
311
207
|
|
|
312
|
-
/// <summary>Lê o <c>entry.json</c>. Ausente ou ilegível devolve nulo — um miss, nunca uma exceção.</summary>
|
|
313
208
|
internal static CacheEntry? ReadEntry(string entryPath)
|
|
314
209
|
{
|
|
315
210
|
if (!File.Exists(entryPath))
|
|
@@ -327,7 +222,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
327
222
|
}
|
|
328
223
|
}
|
|
329
224
|
|
|
330
|
-
/// <summary>O que o <c>entry.json</c> carrega. Sem caminho absoluto nenhum: a árvore é commitada.</summary>
|
|
331
225
|
internal sealed record CacheEntry
|
|
332
226
|
{
|
|
333
227
|
[JsonPropertyName("scenarioName")]
|
|
@@ -336,7 +230,6 @@ internal sealed class ShortPathResponseCache : IDistributedCache
|
|
|
336
230
|
[JsonPropertyName("iterationName")]
|
|
337
231
|
public required string IterationName { get; init; }
|
|
338
232
|
|
|
339
|
-
/// <summary>A chave INTEIRA. É ela que torna o prefixo truncado seguro.</summary>
|
|
340
233
|
[JsonPropertyName("key")]
|
|
341
234
|
public required string Key { get; init; }
|
|
342
235
|
|
|
@@ -1,42 +1,8 @@
|
|
|
1
1
|
namespace Morph.AiKit.Media;
|
|
2
2
|
|
|
3
|
-
/// <summary>
|
|
4
|
-
/// <b>O UNICO ponto de entrada publico da geracao de midia.</b>
|
|
5
|
-
/// </summary>
|
|
6
|
-
/// <remarks>
|
|
7
|
-
/// <para>
|
|
8
|
-
/// Leia as assinaturas: nenhuma delas devolve bytes, stream, <c>BinaryData</c> ou URL de
|
|
9
|
-
/// provider. <see cref="EnqueueAsync"/> devolve um <see cref="MediaJobId"/> e
|
|
10
|
-
/// <see cref="GetAsync"/> devolve o <see cref="MediaJob"/> com o estado dele. Isso e
|
|
11
|
-
/// deliberado, e e o mecanismo — nao a explicacao — da regra dura de
|
|
12
|
-
/// <c>ai-agents/media-pipeline.md</c>:
|
|
13
|
-
/// </para>
|
|
14
|
-
/// <para>
|
|
15
|
-
/// <b>Nunca gere midia dentro de um turno de conversa.</b> Latencia documentada de ate 2
|
|
16
|
-
/// minutos em imagem e de 11 s a 6 min em video, custo por chamada, e zero revisao possivel
|
|
17
|
-
/// dentro do turno. O turno enfileira e confirma; a midia volta depois, revisada.
|
|
18
|
-
/// </para>
|
|
19
|
-
/// <para>
|
|
20
|
-
/// O tipo que efetivamente produz bytes (<c>IMediaProvider</c> e os tres adaptadores de
|
|
21
|
-
/// provider) e <c>internal</c> ao assembly, com <c>InternalsVisibleTo</c> liberando apenas
|
|
22
|
-
/// <c>Morph.AiKit.Tests</c>. Um handler de mensagem no projeto consumidor <b>nao consegue
|
|
23
|
-
/// nomear</b> esse tipo: o codigo nao compila. E a diferenca entre "o standard diz para nao
|
|
24
|
-
/// fazer" e "o compilador recusa", e ha um teste de acessibilidade por reflexao que a
|
|
25
|
-
/// mantem verdadeira.
|
|
26
|
-
/// </para>
|
|
27
|
-
/// </remarks>
|
|
28
3
|
public interface IMediaGenerator
|
|
29
4
|
{
|
|
30
|
-
/// <summary>
|
|
31
|
-
/// Aceita o pedido e devolve o id do job. <b>Nunca devolve bytes.</b>
|
|
32
|
-
/// </summary>
|
|
33
|
-
/// <remarks>
|
|
34
|
-
/// A chave de idempotencia de PEDIDO e consultada aqui, antes de qualquer chamada paga:
|
|
35
|
-
/// um pedido identico a outro ja atendido reusa o job existente em vez de pagar de
|
|
36
|
-
/// novo. O teto de custo tambem recusa aqui — depois da chamada, um alerta chega tarde.
|
|
37
|
-
/// </remarks>
|
|
38
5
|
Task<MediaResult<MediaJobId>> EnqueueAsync(MediaRequest request, CancellationToken cancellationToken = default);
|
|
39
6
|
|
|
40
|
-
/// <summary>O job, com estado, caminho e proveniencia. Job inexistente e recusa nomeada.</summary>
|
|
41
7
|
Task<MediaResult<MediaJob>> GetAsync(MediaJobId id, CancellationToken cancellationToken = default);
|
|
42
8
|
}
|
|
@@ -1,77 +1,21 @@
|
|
|
1
1
|
namespace Morph.AiKit.Media;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
/// O que PRODUZ BYTES. <b><c>internal</c> ao assembly, e e isso que prende a regra dura.</b>
|
|
5
|
-
/// </summary>
|
|
6
|
-
/// <remarks>
|
|
7
|
-
/// <para>
|
|
8
|
-
/// <c>ai-agents/media-pipeline.md</c> diz: <i>nunca gere midia dentro de um turno de
|
|
9
|
-
/// conversa</i>. Uma frase num standard nao prende nada — o que prende e este modificador.
|
|
10
|
-
/// </para>
|
|
11
|
-
/// <para>
|
|
12
|
-
/// Este tipo e os tres adaptadores de provider sao <c>internal</c>, com
|
|
13
|
-
/// <c>InternalsVisibleTo</c> liberando apenas <c>Morph.AiKit.Tests</c>. Um handler de
|
|
14
|
-
/// mensagem no projeto consumidor <b>nao consegue nomear</b> o tipo que produz bytes: o
|
|
15
|
-
/// codigo nao compila. A unica superficie publica e
|
|
16
|
-
/// <see cref="IMediaGenerator.EnqueueAsync"/>, que devolve um <see cref="MediaJobId"/>.
|
|
17
|
-
/// </para>
|
|
18
|
-
/// <para>
|
|
19
|
-
/// Um teste de acessibilidade por reflexao (<c>MediaAccessibilityTests</c>) mantem esta
|
|
20
|
-
/// afirmacao verdadeira: tornar o tipo publico derruba a suite. Um canario que ninguem viu
|
|
21
|
-
/// morder pode ser o canario errado, entao a mutacao foi rodada.
|
|
22
|
-
/// </para>
|
|
23
|
-
/// </remarks>
|
|
3
|
+
// internal e o que impede um handler de gerar midia dentro do turno de conversa; MediaAccessibilityTests cobra.
|
|
24
4
|
internal interface IMediaProvider
|
|
25
5
|
{
|
|
26
|
-
/// <summary>Familia do SDK: <c>openai</c>, <c>google</c>.</summary>
|
|
27
6
|
string Provider { get; }
|
|
28
7
|
|
|
29
|
-
/// <summary>Se este adaptador atende esse tipo de midia.</summary>
|
|
30
8
|
bool Supports(MediaKind kind);
|
|
31
9
|
|
|
32
|
-
/// <summary>
|
|
33
|
-
/// Produz os bytes. Bloqueio de moderacao volta como
|
|
34
|
-
/// <see cref="MediaErrorCode.PolicyBlocked"/> — terminal, nunca retryable; falha
|
|
35
|
-
/// tecnica volta como <see cref="MediaErrorCode.ProviderFailure"/>.
|
|
36
|
-
/// </summary>
|
|
37
|
-
/// <param name="request">O pedido, ja validado.</param>
|
|
38
|
-
/// <param name="binding">
|
|
39
|
-
/// O alias resolvido. Viaja como parametro, e nao no construtor, porque um adaptador
|
|
40
|
-
/// atende N aliases do mesmo provider e uma instancia por alias multiplicaria clientes
|
|
41
|
-
/// sem nenhum ganho.
|
|
42
|
-
/// </param>
|
|
43
|
-
/// <param name="cancellationToken">—</param>
|
|
44
10
|
Task<MediaResult<MediaArtifact>> ProduceAsync(
|
|
45
11
|
MediaRequest request,
|
|
46
12
|
MediaAliasBinding binding,
|
|
47
13
|
CancellationToken cancellationToken);
|
|
48
14
|
}
|
|
49
15
|
|
|
50
|
-
/// <summary>
|
|
51
|
-
/// Constroi os clientes de SDK dos providers de midia. <b>A costura do subsistema</b>: o
|
|
52
|
-
/// unico ponto em que o kit toca um provider de verdade.
|
|
53
|
-
/// </summary>
|
|
54
|
-
/// <remarks>
|
|
55
|
-
/// <para>
|
|
56
|
-
/// Publica de proposito, e pela mesma razao de <c>IProviderClientFactory</c>: e assim que a
|
|
57
|
-
/// suite do kit roda <b>sem rede e sem chave de API</b> — em teste ninguem implementa esta
|
|
58
|
-
/// interface, porque os testes usam um <c>IMediaProvider</c> fake e nunca chegam aqui.
|
|
59
|
-
/// </para>
|
|
60
|
-
/// <para>
|
|
61
|
-
/// Devolver <c>null</c> significa "este provider nao e meu": o catalogo entao procura outro
|
|
62
|
-
/// adaptador, e se nenhum atender, o job falha com motivo nomeado em vez de silenciosamente
|
|
63
|
-
/// nao acontecer.
|
|
64
|
-
/// </para>
|
|
65
|
-
/// <para>
|
|
66
|
-
/// <b>Quem descarta o cliente e o projeto</b>, que o construiu. O kit nao guarda cache
|
|
67
|
-
/// propria de cliente — duas caches da mesma coisa e como um cliente vaza de um teste.
|
|
68
|
-
/// </para>
|
|
69
|
-
/// </remarks>
|
|
70
16
|
public interface IMediaClientFactory
|
|
71
17
|
{
|
|
72
|
-
/// <summary>O <c>ImageClient</c> do alias, ou nulo quando o provider nao e a OpenAI.</summary>
|
|
73
18
|
OpenAI.Images.ImageClient? CreateOpenAiImageClient(MediaAliasBinding binding);
|
|
74
19
|
|
|
75
|
-
/// <summary>O cliente do Google GenAI, ou nulo quando o provider nao e o Google.</summary>
|
|
76
20
|
Google.GenAI.Client? CreateGoogleClient(MediaAliasBinding binding);
|
|
77
21
|
}
|