Partial success responses
A request that asks for five tasks should not return a 500 when one of them fails. The caller wanted sentiment, entities, keywords, summary, and emotion. Four of those came back clean. Surface the four successes, surface the one failure, let the caller decide what to do about it.
Per-task isolation in one picture
Each task runs in its own try block. Successes feed the response body, failures land in the errors map.
class AnalyzeResponse(BaseModel):
text: str
tasks: List[str]
sentiment: Optional[SentimentResult] = None
entities: Optional[List[Entity]] = None
keywords: Optional[List[str]] = None
summary: Optional[str] = None
emotion: Optional[EmotionResult] = None
errors: dict = Field(
default_factory=dict,
description="Map of task name to error message for any task that failed.",
)The errors dict is the key design choice. Task name to error message. Callers can look up errors["summary"] and render a "Summary unavailable" badge in the UI. No exceptions bubble up from the service. The response shape is always valid.
{
"text": "Great product, terrible support.",
"tasks": ["sentiment", "entities", "summary", "emotion"],
"sentiment": { "label": "negative", "score": -0.4, "rationale": "Mixed review with net-negative tilt" },
"entities": [],
"keywords": null,
"summary": "A customer praises the product but criticises support.",
"emotion": null,
"errors": {
"emotion": "Rate limit hit, retry after 30s"
}
}Four tasks ran cleanly, emotion hit a rate limit. The caller gets everything that worked plus a clear signal about what did not. This is partial success done right.
A 500 throws away the work that succeeded. If four tasks cost you four LLM calls and one failed, a 500 means the caller has to retry all five and pay again. Partial success lets the caller keep the good data and only retry the one that failed. It also matches how the UI actually wants to behave: render the sentiment badge, show a small warning on the emotion pill, do not blank the whole page.