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("""
Use AI to transform your project ideas into initial draft funding proposals.