import os import gradio as gr from crewai import Agent, Task, Crew, Process from langchain_openai import AzureChatOpenAI from typing import Dict, Any import traceback from dotenv import load_dotenv # Load environment variables load_dotenv() # Configure Azure OpenAI os.environ["AZURE_API_TYPE"] = "azure" os.environ["AZURE_API_BASE"] = os.getenv("AZURE_OPENAI_ENDPOINT") os.environ["AZURE_API_KEY"] = os.getenv("AZURE_OPENAI_API_KEY") os.environ["AZURE_API_VERSION"] = os.getenv("OPENAI_API_VERSION") os.environ["AZURE_DEPLOYMENT_NAME"] = os.getenv("AZURE_DEPLOYMENT_NAME") # Validate environment variables required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_KEY", "OPENAI_API_VERSION", "AZURE_DEPLOYMENT_NAME"] missing_vars = [var for var in required_vars if not os.getenv(var)] if missing_vars: raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}") # Initialize LLM llm = AzureChatOpenAI( azure_deployment=os.getenv("AZURE_DEPLOYMENT_NAME"), azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), api_key=os.getenv("AZURE_OPENAI_API_KEY"), api_version=os.getenv("OPENAI_API_VERSION"), model=f"azure/{os.getenv('AZURE_DEPLOYMENT_NAME')}", max_retries=3, timeout=30, temperature=0.3, ) # ================= Alternative to use this offline with ollama ================= #os.environ["OPENAI_API_KEY"] = "sk-anything" # Dummy key to bypass LiteLLM checks #os.environ["OLLAMA_HOST"] = "http://localhost:11434" # Force local Ollama #os.environ["OPENAI_API_BASE"] = "http://localhost:11434" #os.environ["OPENAI_API_MODEL"] = "deepseek-r1:8b" #from langchain_ollama import OllamaLLM # Replace your Ollama initialization with this: #llm_deepseek = OllamaLLM( # model="ollama/deepseek-r1:8b", # Note the 'ollama/' prefix # base_url="http://localhost:11434", # temperature=0.7, # timeout=300 #) # ================= AGENTS ================= proposal_writer = Agent( role="Humanitarian Proposal Writer", llm= llm, goal="Write compelling funding proposals for humanitarian projects", backstory=( "You are an expert in humanitarian aid and grant writing. " "You have 10+ years of experience drafting proposals" "and know how to structure them for maximum impact." ), verbose=True, allow_delegation=False ) proposal_reviewer = Agent( role="Proposal Quality Assurance Specialist", llm=llm, goal="Ensure proposals meet funding criteria, follows strict formatting and content requirements and are well-structured", backstory=( "You are a grant evaluator for major humanitarian organizations. " "You have a deep knowledge of donor expectations and proposal evaluation criteria." "You are meticulous about formatting, word limits, and logical flow." "You know exactly what funders look for and how to improve proposals." ), verbose=True, allow_delegation=False ) # ================= TASKS ================= def create_proposal_task(idea: str, donor: str, duration: str, budget: str, scope: str): return Task( description=( f"""Write a detailed funding proposal with: - Project Idea: {idea} - Budget: {budget} - Duration: {duration} - Geographic Scope: {scope} - Targeted Donor: {donor}""" ), expected_output=( "A well-structured proposal document formatted as markdown with clear sections: " "1. Project Summary (350 words)" "2. Rationale (400 words)" "3. Project Description (600 words)" "4. Partnerships and Coordination (Stakeholder mapping, cross-cutting themes -350 words)" "5. Monitoring (350 words)" "6. Evaluation (350 words)" "7. Results Matrix (with Objectives, Outcomes, Outputs & Indicators - formated as a table)" "8. Workplan (formated as a table - with activities, beging and end date, focal point)" "9. Budget (formated as a table: type, unit, number, unit cost, total cost)" "10. Risk Assessment and treatment plan (formated as a table: risks, consequences, likelihood, treatment options: Avoiding the risk - Changing the likelihood of the risk - Changing the consequence of the risk - Sharing the risk with another party - Tolerating the risk without further treatmen, treatment actions)" ), agent=proposal_writer ) def review_task(): return Task( description=( "Critically review the proposal draft and suggest improvements: " "- Ensure that the project idea is achievable within the budget. " "- Ensure that the problem statement and theory of change are well articulated within the project description. " "- Ensure that all selected indicators are S.M.A.R.T. and that methodology for each of them is documented." "- Ensure that the project aligns with the traditional priorities of the targeted donor." "- Ensure clarity, conciness, and no errors." "- Ensure that the proposal is generated in the same language than one used to describe the project." ), expected_output=( "Proposal with implemented changes formatted as markdown with the same sections" ), agent=proposal_reviewer ) # ================= CREW ================= def generate_proposal(idea: str, donor: str, duration: str, budget: str, scope: str): inputs = { "idea": idea, "budget": budget, "duration": duration, "scope": scope, "donor": donor } crew = Crew( agents=[proposal_writer, proposal_reviewer], tasks=[create_proposal_task(**inputs), review_task()], process=Process.sequential, manager_llm=llm, verbose=True, memory=False ) result = crew.kickoff() # Convert CrewOutput to string if hasattr(result, 'export'): return str(result.export().get('output', str(result))) return str(result) # ================= GRADIO INTERFACE ================= with gr.Blocks( title="Humanitarian Proposal Generator - AI Funding Proposal Tool", css=""" @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap'); @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); /* Completely disable Gradio's dark theme */ .gradio-container.dark { --body-background-fill: white !important; --background-fill-primary: white !important; --background-fill-secondary: #f8f9fa !important; --block-background-fill: white !important; --input-background-fill: white !important; --block-label-text-color: #212529 !important; --body-text-color: #212529 !important; --block-title-text-color: var(--primary-color) !important; --border-color-primary: #dee2e6 !important; } .gradio-container.dark .gr-markdown, .gradio-container.dark .gr-textbox, .gradio-container.dark .gr-dropdown, .gradio-container.dark .output-section { background: white !important; color: #212529 !important; border-color: #dee2e6 !important; } /* Base Styles */ :root { --primary-color: #0033A0; --secondary-color: #e67e22; --accent-color: #f59e0b; --dark-color: #34495e; --light-color: #ecf0f1; --success-color: #27ae60; --warning-color: #f39c12; --danger-color: #e74c3c; --text-color: #333; --text-light: #7f8c8d; --border-radius: 8px; --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --transition: all 0.3s ease; } /* Header Styles */ .header { text-align: center; margin-bottom: 2rem; padding: 1rem; } .header h1 { margin: 0; font-family: 'Lato', sans-serif; font-size: 2.5rem; font-weight: 600; color: var(--primary-color); } .header p { margin: 0.5rem 0 0; font-family: 'Lato', sans-serif; opacity: 0.9; font-size: 1.5rem; color: #4b5563; } /* Section Titles */ .section-title { display: flex; align-items: left; font-family: 'Lato', sans-serif; gap: 0.5rem; color: var(--primary-color); margin: 1rem 0; font-size: 1.25rem; font-weight: 600; } .section-title i { font-size: 1.1em; color: var(--accent-color); } /* Input Section */ .input-section { background: white; padding: 0.75rem 0.5rem; border: 1px solid #d1d5db; border-radius: var(--border-radius); box-shadow: var(--box-shadow); margin-right: 1rem; } /* Output Section */ .output-section { background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow); } /* Form Elements */ .gr-textbox, .gr-dropdown { border: 1px solid #ddd; border-radius: var(--border-radius) !important; padding: 0.75rem 1rem !important; transition: var(--transition); } .gr-textbox:focus, .gr-dropdown:focus { border-color: var(--primary-color) !important; box-shadow: 0 0 0 2px rgba(44, 110, 203, 0.2) !important; outline: none !important; } .gr-textbox::placeholder { color: var(--text-light) !important; opacity: 0.7 !important; } label { font-weight: 500 !important; color: var(--dark-color) !important; margin-bottom: 0.5rem !important; display: block !important; } /* Buttons */ .btn-primary { background: var(--primary-color) !important; color: white !important; border: none !important; border-radius: var(--border-radius) !important; padding: 0.75rem 1.5rem !important; font-weight: 500 !important; transition: var(--transition) !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; } .btn-primary:hover { background: #002080 !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; } .btn-primary:active { transform: translateY(0) !important; } /* Output Markdown */ .gr-markdown { background: #f9f9f9; padding: 1.5rem; border-radius: var(--border-radius); border-left: 4px solid var(--primary-color); } /* Debug Console */ .gr-textbox[label="⚠️ Journal d'Exécution"] { font-family: monospace !important; background: #2c3e50 !important; color: #ecf0f1 !important; border-radius: var(--border-radius) !important; padding: 1rem !important; } /* Responsive Layout */ @media (max-width: 768px) { .gr-row { flex-direction: column !important; } .input-section { margin-right: 0 !important; margin-bottom: 1rem !important; } } """, head=''' ''' ) as demo: # Header section with gr.Column(): with gr.Row(): with gr.Column(): gr.HTML("""

Humanitarian Proposal Generator (Demo)

Use AI to transform your project ideas into initial draft funding proposals.

""") # Main content with gr.Row(): # Input column with gr.Column(elem_classes="input-section"): gr.HTML("""

Project Description

""") idea = gr.Textbox( label="Describe your Project Idea", lines=4, placeholder="The more details you can provide, the better... Review the results and the review at the end of the output, then iterate to get better output." ) gr.HTML("""
Proposal Context
""") with gr.Row(): with gr.Column(): donor = gr.Dropdown( ["ECHO","European Union", "USAID", "UK FCDO", "Germany BMZ", "SIDA", "Global Affairs Canada", "IOM", "International Finance Corporation", "Islamic Development Bank","Japan - Ministry of Foreign Affairs"], label="Targeted Donor", value="IOM" ) duration = gr.Dropdown( ["6 months", "1 year", "2 years", "3 years"], label="Project Duration", value="1 year" ) with gr.Column(): budget = gr.Dropdown( ["$50,000", "$100,000", "$150,000","$250,000$","$500,000","$1000,000","$1,500,000","$2,500,000","$5,000,000","$10,000,000" ], label="Budget", value="$150,000" ) scope = gr.Textbox( label="Geographic Scope", placeholder="Country, Region, etc..." ) with gr.Row(): generate_btn = gr.Button( value="✨ Generate Proposal", variant="primary", elem_classes="btn-primary" ) with gr.Column(elem_classes="output-section"): output = gr.Markdown( label="AI-Generated Proposal" ) debug = gr.Textbox( label="⚠️ Execution Log", interactive=False ) # New way to add JavaScript without _js parameter demo.load( None, None, None, js=""" function() { console.log('Interface loaded successfully'); return []; } """ ) def process_proposal(idea, donor, duration, budget, scope): debug_log = "🚀 Starting proposal document generation...\n" try: if not idea.strip(): raise ValueError("Project idea cannot be empty") debug_log += f"✅ Received inputs:\n- Description: {idea[:50]}...\n- Donor: {donor}\n- Duration: {duration}\n- Budget: {budget}\n- Scope: {scope}\n" result = generate_proposal(idea, donor, duration, budget, scope) debug_log += "🎉 Proposal generated successfully!\n" return result, debug_log except Exception as e: error_msg = f"❌ Error: {str(e)}\n{traceback.format_exc()}" debug_log += error_msg return f"## Error\n```\n{error_msg}\n```", debug_log generate_btn.click( process_proposal, inputs=[idea, donor, duration, budget, scope], outputs=[output, debug] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", share=False, debug=True, show_error=True)