Skip to content

Add text input layer: POST /input/text and GET /input/{input_id} (#546)#595

Open
abhishek-8081 wants to merge 2 commits into
fireform-core:developmentfrom
abhishek-8081:issue-546-text-input
Open

Add text input layer: POST /input/text and GET /input/{input_id} (#546)#595
abhishek-8081 wants to merge 2 commits into
fireform-core:developmentfrom
abhishek-8081:issue-546-text-input

Conversation

@abhishek-8081

Copy link
Copy Markdown
Collaborator

Closes #546.

Adds the text input layer — POST /api/v1/input/text and GET /api/v1/input/{input_id} — building on the Input model from #544. Fully synchronous (no Celery); text is stored and marked ready immediately. No migration needed (the inputs table already exists).

Validation follows the contract, which specifies the narrative minimum two ways — minLength: 20 in the schema and "at least 10 words" in the description and 422 example. Enforces both: min_length=20 via Pydantic (auto-422 through the #543 handler) plus a 10-word check in the route. The word-count 422 is emitted as a direct JSONResponse matching the #543 validation-error shape exactly ({error_code, message, validation_errors:[{field, issue, value}]}) — it doesn't use AppError because AppError doesn't carry a validation_errors list, and the goal is a single consistent 422 format. The >50,000-char limit raises a 413 NARRATIVE_TOO_LONG (manual AppError, since the contract wants 413 not 422). GET on an unknown id returns the #543 envelope with INPUT_NOT_FOUND.

retry_after_seconds on the GET response is null for ready text inputs; the poll interval (5s, from the contract's voice example) is centralized as INPUT_POLL_INTERVAL_SECONDS in config.py, distinct from the existing RETRY_AFTER_SECONDS (the 503 hint) — it'll be exercised properly by the voice path in #547.

Tests: 127 passing — 15 new in test_v1_input.py covering the 201 happy path, character/word counts, optional-field roundtrip, transcript storage, the 413 boundary (both > and exactly 50k), both 422 paths with exact shape assertions, and GET 200/404.

@chetanr25 chetanr25 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested both APIs and they work well. You've also covered the validations, which is great. From a functionality perspective, this PR is good to go.

That said, I have a few changes in mind, so I've requested changes mainly to keep our codebase maintainable.

Comment thread app/api/router.py Outdated

from app.api.routes import forms, templates, weather, zipcode, jobs, system
from app.api.routes import forms, jobs, system, templates, weather, zipcode
from app.api.routes import input as input_routes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small change.
As per the Discussion comment, we chose to use non-Alias approach.
To keep things constant through-out the codebase, we can follow the approach which we discussed in the Community discussion

Suggested change
from app.api.routes import input as input_routes
from app.api.routes import input

Comment thread app/api/router.py Outdated
api_router.include_router(weather.router, prefix=API_PREFIX)
api_router.include_router(zipcode.router, prefix=API_PREFIX) No newline at end of file
api_router.include_router(zipcode.router, prefix=API_PREFIX)
api_router.include_router(input_routes.router, prefix=API_PREFIX) No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
api_router.include_router(input_routes.router, prefix=API_PREFIX)
api_router.include_router(input.router, prefix=API_PREFIX)

Comment thread app/api/routes/input.py Outdated

words = narrative.split()
if len(words) < 10:
return JSONResponse(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use validation_error_handler() in order to throw 422 error. In this way we can avoid duplication and keep our codebase more maintainable

Reference:

async def validation_error_handler(request: Request, exc: RequestValidationError):
validation_errors = []
for error in exc.errors():
loc = error.get("loc", ())
field = ".".join(str(x) for x in loc if x != "body")
validation_errors.append({
"field": field or None,
"issue": error.get("msg"),
"value": error.get("input"),
})
return JSONResponse(
status_code=422,
content={
"error_code": "VALIDATION_ERROR",
"message": "Request validation failed",
"validation_errors": validation_errors,
},
)

Comment thread app/api/routes/input.py Outdated
created_at=now,
updated_at=now,
)
db.add(record)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this to repository.py and use that function here. Let's keep in mind that we will be using repositories.py for all database operations

Comment thread app/api/schemas/input.py Outdated


class TextInputRequest(BaseModel):
model_config = ConfigDict()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, Do you think we would need model_config in Text input?

I think we can skip. Let me know if we are using model_config for anything else (now or in the future)

Suggested change
model_config = ConfigDict()

Comment thread app/api/schemas/input.py Outdated


class TextInputResponse(BaseModel):
model_config = ConfigDict()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes here

Suggested change
model_config = ConfigDict()

Comment thread app/api/routes/input.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is too much going on under this file (keeping in mind that it's a routes file)
We should keep all the service logic under services and routes files would be responsible only to call those functions and expose it as API.

Right now app/api/routes/input.py is serving as:

  • Logic and validation layer (which services/ should be handling)
  • Database operations (which repository.py is responsible for)

@abhishek-8081

abhishek-8081 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

I tested both APIs and they work well. You've also covered the validations, which is great. From a functionality perspective, this PR is good to go.

That said, I have a few changes in mind, so I've requested changes mainly to keep our codebase maintainable.

All make sense, thanks for the thorough review - agree on all of these.

Plan: DB ops (create/get input) move to repositories.py, the word-count 422 reuses
validation_error_handler instead of the manual JSONResponse, non-alias import per #577,
and dropping the empty model_config.

One small check on the service layer: for text input the logic is pretty thin (length/
word validation + char/word counts), so a service would mostly be a pass-through to the
repository. Happy to add app/services/inputs/ with that logic to keep the layering
consistent - just confirming you'd rather have the explicit service even when it's light,
vs. keeping minimal validation in the route. I'll go with the full service layer unless
you say otherwise.

@chetanr25

Copy link
Copy Markdown
Collaborator

I always prefer keeping files as small as possible, with each file responsible only for the functionality it's supposed to handle. For example, routes/ should only expose the APIs, while the actual business logic lives in services/.

The overall flow should be like:

routes -> services -> repository.py -> actual database

Since the scope of splitting this into services is relatively small, I think either approach would work.
also, we will need to create services/input.py anyway when we implement the voice input APIs in #547.
Personally, I prefer splitting this into services, but I'll leave the final decision to @marcvergees.

@marcvergees

Copy link
Copy Markdown
Member

I'll check it soon.

@abhishek-8081

Copy link
Copy Markdown
Collaborator Author

Addressed all the review comments: DB ops moved to repositories.py (create_input /
get_input), logic pulled into a new services/input.py (InputService — pure logic, no
session, persistence stays in the repository), the route is now thin, the word-count 422
goes through validation_error_handler via a RequestValidationError instead of a manual
JSONResponse, non-alias import per #577, and dropped the empty model_config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text input layer - API Contracts

3 participants