curl --request POST \
--url https://api.example.com/api/proposals/{proposal_id}/analyze-concept \
--header 'Content-Type: application/json' \
--data '{
"force": true
}'{
"status": "<string>",
"message": "<string>",
"started_at": "<string>",
"cached": true,
"concept_analysis": {
"sections_needing_elaboration": [
{
"title": "<string>",
"current_content": "<string>",
"gaps": [
{}
],
"suggestions": [
{}
]
}
],
"alignment_score": 123,
"strengths": [
{}
],
"concerns": [
{}
]
},
"completed_at": "<string>",
"error": "<string>"
}curl --request POST \
--url https://api.example.com/api/proposals/{proposal_id}/analyze-concept \
--header 'Content-Type: application/json' \
--data '{
"force": true
}'{
"status": "<string>",
"message": "<string>",
"started_at": "<string>",
"cached": true,
"concept_analysis": {
"sections_needing_elaboration": [
{
"title": "<string>",
"current_content": "<string>",
"gaps": [
{}
],
"suggestions": [
{}
]
}
],
"alignment_score": 123,
"strengths": [
{}
],
"concerns": [
{}
]
},
"completed_at": "<string>",
"error": "<string>"
}/analyze-concept returns immediatelyAnalysisWorkerFunction with analysis_type: "concept"/concept-status for completionPROP-YYYYMMDD-XXXX)true, forces a new analysis even if one already exists. Use this when the concept document has been re-uploaded.processing: Analysis started successfullycompleted: Analysis already exists (cached)true if returning cached resultscurl -X POST "https://api.igad-innovation.org/api/proposals/PROP-20260304-A1B2/analyze-concept" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "force": false }'
{
"status": "processing",
"message": "Concept analysis started. Poll /concept-status for completion.",
"started_at": "2026-03-04T10:35:00.000Z"
}
{
"status": "completed",
"concept_analysis": {
"sections_needing_elaboration": [
{
"title": "Technical Architecture",
"current_content": "We will build a scalable platform...",
"gaps": ["Missing technology stack details", "No scalability metrics"],
"suggestions": ["Specify database technology", "Define expected user load"]
}
],
"alignment_score": 75,
"strengths": ["Clear problem statement", "Well-defined target audience"],
"concerns": ["Budget justification lacks detail"]
},
"message": "Concept already analyzed",
"cached": true
}
force: true to trigger a fresh analysis:
const reanalyzeAfterUpload = async (proposalId: string) => {
const response = await fetch(
`/api/proposals/${proposalId}/analyze-concept`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ force: true })
}
)
return response.json()
}
force: true is provided, the endpoint removes:
REMOVE concept_analysis,
concept_analysis_completed_at,
concept_analysis_error,
concept_evaluation,
concept_document_v2,
structure_workplan_analysis,
structure_workplan_completed_at,
structure_workplan_error
SET analysis_status_concept = :not_started
analysis_status_concept field tracks the analysis state:
| Status | Description |
|---|---|
not_started | No analysis triggered yet |
processing | Lambda worker is analyzing |
completed | Analysis finished successfully |
failed | Analysis encountered an error |
const pollConceptStatus = async (proposalId: string) => {
const maxAttempts = 100 // 5 minutes at 3-second intervals
let attempts = 0
const interval = setInterval(async () => {
attempts++
if (attempts > maxAttempts) {
clearInterval(interval)
throw new Error('Analysis timeout')
}
const response = await fetch(
`/api/proposals/${proposalId}/concept-status`,
{ headers: { Authorization: `Bearer ${token}` } }
)
const data = await response.json()
if (data.status === 'completed') {
clearInterval(interval)
handleConceptAnalysis(data.concept_analysis)
} else if (data.status === 'failed') {
clearInterval(interval)
showError(data.error)
}
}, 3000)
}
lambda_client.invoke(
FunctionName=worker_function_arn,
InvocationType="Event", # Async
Payload=json.dumps({
"proposal_id": proposal_code, # PROP-YYYYMMDD-XXXX format
"analysis_type": "concept"
})
)
await db_client.update_item(
pk=pk,
sk="METADATA",
update_expression="SET analysis_status_concept = :status, concept_analysis_started_at = :started",
expression_attribute_values={
":status": "processing",
":started": datetime.utcnow().isoformat()
}
)
db_client.update_item_sync(
pk=pk,
sk="METADATA",
update_expression="SET analysis_status_concept = :status, concept_analysis = :analysis, concept_analysis_completed_at = :completed",
expression_attribute_values={
":status": "completed",
":analysis": result,
":completed": datetime.utcnow().isoformat()
}
)
{
"detail": "RFP analysis must be completed first"
}
{
"detail": "Concept document not found. Please upload a concept document or provide initial concept text."
}
{
"detail": "Access denied"
}
{
"detail": "Proposal not found"
}
{
"detail": "Concept analysis failed: Worker Lambda invocation error"
}
not_started, processing, completed, or failedShow concept_analysis structure
{
"status": "completed",
"concept_analysis": {
"sections_needing_elaboration": [
{
"title": "Methodology",
"current_content": "We will use agile development practices...",
"gaps": [
"No sprint duration specified",
"Missing stakeholder engagement plan"
],
"suggestions": [
"Define sprint length (1-2 weeks recommended)",
"Outline monthly stakeholder review meetings"
]
},
{
"title": "Risk Management",
"current_content": "We have identified potential technical risks...",
"gaps": [
"No mitigation strategies provided"
],
"suggestions": [
"Add specific mitigation actions for each identified risk"
]
}
],
"alignment_score": 78,
"strengths": [
"Clear understanding of target beneficiaries",
"Realistic timeline",
"Strong team qualifications"
],
"concerns": [
"Budget distribution heavily weighted toward personnel costs",
"Limited discussion of sustainability post-project"
]
},
"completed_at": "2026-03-04T10:37:23.000Z"
}
{
"status": "processing",
"started_at": "2026-03-04T10:35:00.000Z"
}
{
"status": "failed",
"error": "Unable to parse concept document format"
}
status: "completed" is returned immediately with data, skip the polling step entirely.