curl --request POST \
--url https://api.example.com/api/proposals/{proposal_id}/analyze-draft-feedback \
--header 'Content-Type: application/json' \
--data '{
"force": true
}'{
"status": "<string>",
"message": "<string>",
"started_at": "<string>",
"cached": true,
"data": {
"overall_score": 123,
"overall_feedback": "<string>",
"sections": [
{
"title": "<string>",
"score": 123,
"strengths": [
{}
],
"improvements": [
{}
],
"missing_elements": [
{}
]
}
],
"alignment_with_rfp": {
"met_requirements": [
{}
],
"partially_met": [
{}
],
"unmet_requirements": [
{}
]
},
"recommendations": [
{}
]
},
"completed_at": "<string>",
"error": "<string>"
}curl --request POST \
--url https://api.example.com/api/proposals/{proposal_id}/analyze-draft-feedback \
--header 'Content-Type: application/json' \
--data '{
"force": true
}'{
"status": "<string>",
"message": "<string>",
"started_at": "<string>",
"cached": true,
"data": {
"overall_score": 123,
"overall_feedback": "<string>",
"sections": [
{
"title": "<string>",
"score": 123,
"strengths": [
{}
],
"improvements": [
{}
],
"missing_elements": [
{}
]
}
],
"alignment_with_rfp": {
"met_requirements": [
{}
],
"partially_met": [
{}
],
"unmet_requirements": [
{}
]
},
"recommendations": [
{}
]
},
"completed_at": "<string>",
"error": "<string>"
}/upload-draft-proposal/analyze-draft-feedback starts analysisanalysis_type: "draft_feedback"/draft-feedback-status for completionPROP-YYYYMMDD-XXXX)true, forces a new analysis even if one already exists. Use this when the draft has been re-uploaded or significantly edited.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-draft-feedback" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "force": false }'
{
"status": "processing",
"message": "Draft feedback analysis started. Poll /draft-feedback-status for completion.",
"started_at": "2026-03-04T11:15:00.000Z"
}
{
"status": "completed",
"message": "Draft feedback already analyzed",
"data": {
"overall_score": 82,
"sections": [
{
"title": "Executive Summary",
"score": 90,
"strengths": ["Clear and concise", "Addresses key points"],
"improvements": ["Add budget highlight"]
}
]
},
"cached": true
}
const uploadDraft = async (proposalId: string, file: File) => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch(
`/api/proposals/${proposalId}/upload-draft-proposal`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
}
)
if (!response.ok) {
throw new Error('Upload failed')
}
return response.json()
}
force: true:
const reanalyzeDraft = async (proposalId: string) => {
const response = await fetch(
`/api/proposals/${proposalId}/analyze-draft-feedback`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ force: true })
}
)
return response.json()
}
REMOVE draft_feedback_analysis,
draft_feedback_completed_at,
draft_feedback_error
SET analysis_status_draft_feedback = :not_started
analysis_status_draft_feedback field tracks analysis state:
| Status | Description |
|---|---|
not_started | No analysis triggered |
processing | Lambda worker is analyzing |
completed | Analysis finished successfully |
failed | Analysis encountered an error |
const pollFeedbackStatus = async (proposalId: string) => {
const maxAttempts = 100 // 5 minutes
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}/draft-feedback-status`,
{ headers: { Authorization: `Bearer ${token}` } }
)
const data = await response.json()
if (data.status === 'completed') {
clearInterval(interval)
displayFeedback(data.data)
} 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": "draft_feedback"
})
)
await db_client.update_item(
pk=pk,
sk="METADATA",
update_expression="SET analysis_status_draft_feedback = :status, draft_feedback_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_draft_feedback = :status, draft_feedback_analysis = :feedback, draft_feedback_completed_at = :completed",
expression_attribute_values={
":status": "completed",
":feedback": feedback_result,
":completed": datetime.utcnow().isoformat()
}
)
{
"detail": "Step 1 (RFP analysis) must be completed before draft feedback analysis."
}
{
"detail": "Please upload your draft proposal first."
}
{
"detail": "Access denied"
}
{
"detail": "Proposal not found"
}
{
"detail": "Failed to start draft feedback analysis: Worker invocation error"
}
not_started, processing, completed, or failedShow data structure
{
"status": "completed",
"started_at": "2026-03-04T11:15:00.000Z",
"completed_at": "2026-03-04T11:17:45.000Z",
"data": {
"overall_score": 82,
"overall_feedback": "Strong proposal with clear objectives and methodology. Main areas for improvement are budget justification and sustainability planning.",
"sections": [
{
"title": "Executive Summary",
"score": 90,
"strengths": [
"Concise and impactful opening",
"Clearly states problem and solution",
"Strong value proposition"
],
"improvements": [
"Add a one-sentence budget summary",
"Include expected impact metrics"
],
"missing_elements": []
},
{
"title": "Technical Approach",
"score": 85,
"strengths": [
"Well-defined architecture",
"Addresses scalability concerns",
"Good use of diagrams"
],
"improvements": [
"Expand on security measures",
"Add more detail on API design"
],
"missing_elements": [
"Data backup and recovery strategy"
]
},
{
"title": "Budget",
"score": 70,
"strengths": [
"Detailed line items",
"Reasonable cost estimates"
],
"improvements": [
"Add justification for personnel costs",
"Include cost breakdown by project phase",
"Compare with similar projects"
],
"missing_elements": [
"Indirect costs breakdown",
"Currency exchange risk mitigation"
]
}
],
"alignment_with_rfp": {
"met_requirements": [
"Technical specifications",
"Timeline and milestones",
"Team qualifications"
],
"partially_met": [
"Budget justification (missing indirect costs)",
"Risk management (security gaps)"
],
"unmet_requirements": [
"Post-project sustainability plan",
"Local capacity building strategy"
]
},
"recommendations": [
"Priority 1: Add comprehensive sustainability section addressing post-project funding and knowledge transfer",
"Priority 2: Expand security section with specific protocols and compliance measures",
"Priority 3: Provide detailed budget justification including comparison with similar initiatives",
"Priority 4: Include metrics for measuring success and impact"
]
}
}
{
"status": "processing",
"started_at": "2026-03-04T11:15:00.000Z",
"completed_at": null,
"error": null
}
{
"status": "failed",
"started_at": "2026-03-04T11:15:00.000Z",
"error": "Failed to extract text from draft document. Please ensure the file is not corrupted."
}
<div className="feedback-summary">
<h2>Overall Score: {data.overall_score}/100</h2>
<p>{data.overall_feedback}</p>
</div>
{data.sections.map(section => (
<div key={section.title} className="section-feedback">
<h3>
{section.title}
<span className="score">{section.score}/100</span>
</h3>
<div className="strengths">
<h4>✓ Strengths</h4>
<ul>
{section.strengths.map(s => <li>{s}</li>)}
</ul>
</div>
<div className="improvements">
<h4>→ Improvements</h4>
<ul>
{section.improvements.map(i => <li>{i}</li>)}
</ul>
</div>
{section.missing_elements.length > 0 && (
<div className="missing">
<h4>⚠ Missing Elements</h4>
<ul>
{section.missing_elements.map(m => <li>{m}</li>)}
</ul>
</div>
)}
</div>
))}
<div className="rfp-alignment">
<h3>RFP Requirements</h3>
<div className="met">
<h4>✓ Met ({data.alignment_with_rfp.met_requirements.length})</h4>
{data.alignment_with_rfp.met_requirements.map(req => (
<div className="requirement-item complete">{req}</div>
))}
</div>
<div className="partial">
<h4>⚡ Partially Met ({data.alignment_with_rfp.partially_met.length})</h4>
{data.alignment_with_rfp.partially_met.map(req => (
<div className="requirement-item partial">{req}</div>
))}
</div>
<div className="unmet">
<h4>✗ Not Met ({data.alignment_with_rfp.unmet_requirements.length})</h4>
{data.alignment_with_rfp.unmet_requirements.map(req => (
<div className="requirement-item missing">{req}</div>
))}
</div>
</div>
force: true