feat(api): conversation HTTP endpoints + server-side scenario seeding (spec 004)#206
Open
davidortinau wants to merge 6 commits into
Open
feat(api): conversation HTTP endpoints + server-side scenario seeding (spec 004)#206davidortinau wants to merge 6 commits into
davidortinau wants to merge 6 commits into
Conversation
Implements specs/004-conversation-http-endpoints/spec.md — exposes the
Conversation activity to the Flutter client over HTTP.
Endpoints (all under /api/v1/conversation, RequireAuthorization):
- GET /scenarios → PascalCase scenarios list (matches MAUI EF shape)
- POST /start → opening assistant message for a scenario
- POST /continue → next assistant turn + grading (comprehension score
+ grammar corrections + vocabulary analysis)
Server-side service is stateless: the client passes full history every
turn. Why a new server-side IServerConversationService rather than
reusing the MAUI IConversationAgentService:
- AppLib is UseMaui=true / net11; the API is net10.0 and cannot
reference it.
- The MAUI impl is stateful (holds _conversationAgent on the instance);
wrong model for HTTP.
- The MAUI impl loads prompts via MAUI's OpenAppPackageFileAsync; the
API's IFileSystemService stub throws on that method.
ServerConversationService inlines the 4 scriban prompt templates as
C# string interpolation and runs partner + grader IChatClient calls in
parallel on /continue. Defensive score normalization handles both 0–1
and 0–100 grader scales (Math.Round, clamp to [0,100]).
DI hardening in Program.cs (fixes pre-existing test breakage):
The unconditional Singleton registrations of AiService,
TranscriptFormattingService, VideoImportPipelineService, and
IVoiceDiscoveryService were causing ValidateOnBuild to fail any test
host that didn't configure AI:OpenAI:ApiKey or ElevenLabsKey, because
those services transitively require IChatClient / ElevenLabsClient
(which are themselves registered conditionally). Moved each into the
conditional block alongside its underlying dependency. The new
IServerConversationService follows the same pattern; endpoints return
503 when it isn't registered (parity with /api/v1/ai/chat).
Wire-contract enforcement:
- /scenarios DTO uses explicit [JsonPropertyName] PascalCase keys
because minimal API defaults to camelCase.
- /start and /continue use default camelCase.
- Tests assert both directions (keys present and not-leaking).
Tests: 17 new (9 endpoint integration + 8 score-normalization theory
cases). All pass. Full API.Tests suite: 182/183 — the one remaining
failure (API_PlansEndpointWorksWithValidToken returning 401) is a
pre-existing latent test issue that was previously masked by the DI
validation failure and is unrelated to this work.
Out of scope per spec: sessionized memory, per-bubble TTS, plan timer,
vocabulary scoring write-back. vocabularyAnalysis is emitted as an
empty array for v1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a with MAUI The 5 predefined Conversation scenarios were previously seeded only by the MAUI client into each device's local SQLite. The Flutter client has no local DB and reaches the API directly, so GET /api/v1/conversation/scenarios returned an empty list against Postgres. Move the seed data to a single shared source (Shared/Data/ConversationScenarioSeedData.cs) consumed by both: - Api: new ConversationScenarioSeeder runs from Program.cs startup alongside NumberContentSeeder. Idempotent upsert keyed on (Name, IsPredefined=true). Updates existing predefined rows when seed copy changes; leaves user-created rows alone. - AppLib: ScenarioService.SeedPredefinedScenariosAsync now consumes the shared data. Same call site (SentenceStudioAppBuilder), same idempotent HasPredefinedScenariosAsync gate — preserves offline-first-launch UX. Both heads seed independently (mirrors the NumberContentSeeder precedent); ConversationScenario is intentionally NOT added to CoreSync — model has no UserProfileId yet and existing locally-seeded rows would collide on id. Tests: 5 new ConversationScenarioSeederTests cover empty / idempotent / stale-update / partial-state / user-row-untouched scenarios. Verified: - dotnet build src/SentenceStudio.Api → 0 errors - dotnet test tests/SentenceStudio.Api.Tests → 187/188 (1 pre-existing unrelated failure: API_PlansEndpointWorksWithValidToken) - aspire run --detach --isolated → AppHost up, API resource Ready, no startup errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Npgsql's retrying execution strategy disallows user-initiated transactions unless they run inside IExecutionStrategy.ExecuteAsync. Production deploy crashed the API container on startup with: InvalidOperationException: The configured execution strategy 'NpgsqlRetryingExecutionStrategy' does not support user-initiated transactions. Wrap the BeginTransaction + pg_advisory_xact_lock + SeedCoreAsync sequence in a strategy delegate so it's retriable as a unit. SeedCoreAsync now also clears the change tracker at entry so a retry runs against a clean state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tory
Production /api/v1/plans/today (and other plans endpoints) returned 500 with:
UnauthorizedAccessException: Access to the path '/app/sentencestudio' is denied
---> IOException: Permission denied
at ApiFileSystemService..ctor(String appDataDirectory)
at TenantContextMiddleware.InvokeAsync(...)
ACA's /app filesystem is read-only for the runtime user, so
Environment.SpecialFolder.LocalApplicationData (→ /app/sentencestudio)
threw on Directory.CreateDirectory. The singleton constructs lazily on
first transitive resolution; plans pulls it in via PlanService →
LearningResourceRepository, so the first plans request blew the ctor.
Conversation doesn't depend on it transitively, which is why that
endpoint worked.
The API doesn't actually persist anything in AppDataDirectory — the
dependency is structural (the abstraction exists for MAUI). Default
to Path.GetTempPath() (/tmp in ACA, always writable), with an
AppDataDirectory config override for ops flexibility.
Also harden ApiFileSystemService.ctor to swallow UnauthorizedAccessException
and IOException on the CreateDirectory call so DI resolution can never
crash the request pipeline if a future deployment lands on a read-only
mount.
Credit: diagnosis from SentenceStudioFlutter session (Azure Log Analytics
trace + repro).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements GET /api/v1/activity-log per Flutter handoff spec (activity-log-api-spec.md). Extracts the per-user activity rollup logic out of ProgressService into a new IActivityLogService so the API and MAUI share one code path; ProgressService.GetActivityLogAsync now delegates to it. Endpoint contract: - Auth: Bearer JWT required (401 otherwise). - Query: fromUtc, toUtc (required, ISO-8601), filter=All|Input|Output (optional). - Validation: 400 on missing dates, fromUtc>toUtc, range >90 days, bad filter. - Body: [] when no activity; otherwise Monday-anchored 7-day weeks newest-first with camelCase keys (weekStart, weekEnd, hasActivity, inputMinutes, outputMinutes, completedAtUtc, etc.) and PascalCase enum values (Input/Output, Reading/Writing/...). - Ad-hoc plans (PlanItemId prefixed "adhoc-") surface as isAdhoc=true with displayName="Ad-hoc"; generated plans within a day are numbered "Plan 1", "Plan 2", ... Registers IActivityLogService scoped on the API side (consumes the request-scoped IUserScopeProvider via HttpUserScopeProvider) and singleton on the MAUI side (DeviceUserScopeProvider is a singleton). Tests: 10 new endpoint tests cover auth, validation, empty result, rollup wire shape, ad-hoc labeling, filter behavior, and per-user isolation. Full API suite 197/198 (the 1 pre-existing PlansEndpoint failure is unrelated and was red on main before this work). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round 2 of the Flutter-unblock contract. No MAUI breakage.
Activity Log DTO widened to match the spec:
- Week: totalMinutes, inputMinutes, outputMinutes, activityCount,
plansCompleted, plansTotal.
- Entry: planItemId, estimatedMinutes, isCompleted, resourceTitle
(nullable), skillName (nullable), title, description. title and
description currently pass TitleKey/DescriptionKey through unchanged
(e.g. "PlanItemReadingTitle") until server-side localization is
wired up — the spec explicitly permits this as temporary.
New endpoint: POST /api/v1/plans/adhoc/start
- Auth: Bearer JWT, user resolved via IUserScopeProvider.
- Idempotent via deterministic planItemId = "adhoc-{clientSessionId}".
No schema change; the (UserProfileId, Date, PlanItemId) lookup is
the uniqueness contract.
- 201 Created on first call; 200 OK on idempotent replay (same id).
- 400 problem+json for missing/invalid clientSessionId, unknown
activityType, non-positive estimatedMinutes.
- estimatedMinutes defaults to 10 when omitted; clamps at 24h max.
- Returned planItemId is accepted by the existing
POST /api/v1/plans/{date}/items/{id}/progress endpoint (covered
by a test).
Tests: 10 new ad-hoc + 1 widened activity-log test. Full API suite
207/208 (1 pre-existing unrelated JwtBearer failure on main).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements spec 004 to expose MAUI's `IConversationAgentService` behavior as HTTP endpoints so a Flutter client can drive the Conversation activity. Also moves predefined scenario seeding to the API server so the Flutter client (which has no local DB) sees the canned set.
Endpoints (all under `/api/v1/conversation`, all `RequireAuthorization`)
Endpoints return 503 when the API has no `IChatClient` configured (matches the `/api/v1/ai/chat` pattern).
Server-side scenario seeding
DI cleanup (side fix surfaced by ValidateOnBuild)
Moved `AiService`, `TranscriptFormattingService`, `VideoImportPipelineService` into the `if (openAiApiKey)` conditional block, and `IVoiceDiscoveryService` into the `if (elevenLabsKey)` block. These services require `IChatClient` / `ElevenLabsClient` (themselves conditional), so unconditional registration crashed host startup whenever the keys were absent. Endpoints that depend on these services already accept them as nullable `[FromServices]` and return 503 when null.
Tests
Verification
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com