Commit
·
cd0ab55
1
Parent(s):
4198319
feat: added dynamic config support for ai endpoints
Browse files- api/main.py +78 -83
- core/strategist.py +23 -6
api/main.py
CHANGED
|
@@ -82,11 +82,15 @@ class ChatResponseRequest(BaseModel): prompt: str = Field(..., description="The
|
|
| 82 |
class ChatResponsePayload(BaseModel): response: str
|
| 83 |
class CaptionRequest(BaseModel): caption: str; action: str
|
| 84 |
class CaptionResponse(BaseModel): new_caption: str
|
| 85 |
-
class BudgetRequest(BaseModel):
|
|
|
|
|
|
|
| 86 |
class BudgetResponse(BaseModel): predicted_budget_usd: float
|
| 87 |
class MatcherRequest(BaseModel): campaign_description: str; target_audience_age: str; target_audience_gender: str; engagement_rate: float; followers: int; country: str; niche: str
|
| 88 |
class MatcherResponse(BaseModel): suggested_influencer_ids: List[int]
|
| 89 |
-
class PerformanceRequest(BaseModel):
|
|
|
|
|
|
|
| 90 |
class PerformanceResponse(BaseModel): predicted_engagement_rate: float; predicted_reach: int
|
| 91 |
class StrategyRequest(BaseModel): prompt: str
|
| 92 |
class StrategyResponse(BaseModel): response: str
|
|
@@ -121,9 +125,13 @@ class RankedCampaignResult(BaseModel): campaign_id: int; score: float
|
|
| 121 |
class RankCampaignsResponse(BaseModel): ranked_campaigns: List[RankedCampaignResult]
|
| 122 |
class CaptionAssistRequest(BaseModel): caption: str; action: str = Field(..., description="Action to perform: 'improve', 'hashtags', or 'check_guidelines'"); guidelines: Optional[str] = None
|
| 123 |
class CaptionAssistResponse(BaseModel): new_text: str
|
| 124 |
-
class ForecastRequest(BaseModel):
|
|
|
|
|
|
|
| 125 |
class PerformanceForecast(BaseModel): predicted_engagement_rate: float; predicted_reach: int
|
| 126 |
-
class
|
|
|
|
|
|
|
| 127 |
class ForecastResponse(BaseModel): performance: PerformanceForecast; payout: PayoutForecast
|
| 128 |
class InfluencerKpiData(BaseModel): totalReach: int; totalLikes: int; totalComments: int; avgEngagementRate: float; totalSubmissions: int
|
| 129 |
class InfluencerAnalyticsSummaryResponse(BaseModel): summary: str
|
|
@@ -163,6 +171,14 @@ class WeeklyPlanContext(BaseModel): niche: str; current_mood: str; recent_achiev
|
|
| 163 |
class WeeklyPlanRequest(BaseModel): context: WeeklyPlanContext
|
| 164 |
class PlanOption(BaseModel): type: str; title: str; platform: str; contentType: str; instructions: str; reasoning: str
|
| 165 |
class WeeklyPlanResponse(BaseModel): options: List[PlanOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
# --- FastAPI App ---
|
| 168 |
app = FastAPI(title="Reachify AI Service (Deploy-Ready)", version="11.0.0")
|
|
@@ -395,11 +411,18 @@ async def generate_strategy_route(request: StrategyRequest):
|
|
| 395 |
except Exception as e:
|
| 396 |
raise HTTPException(status_code=500, detail=f"An internal error occurred in the AI model: {e}")
|
| 397 |
|
| 398 |
-
@app.post("/api/v1/predict/budget", response_model=BudgetResponse
|
| 399 |
async def predict_budget(request: BudgetRequest):
|
| 400 |
-
if not _budget_predictor: raise HTTPException(status_code=503, detail="
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
return BudgetResponse(predicted_budget_usd=round(prediction, 2))
|
| 404 |
|
| 405 |
@app.post("/api/v1/match/influencers", response_model=MatcherResponse, summary="Match Influencers to Campaign")
|
|
@@ -658,36 +681,17 @@ def generate_weekly_summary_route(request: WeeklySummaryRequest):
|
|
| 658 |
print(f"🚨 An error occurred in /strategist/generate-weekly-summary: {e}")
|
| 659 |
raise HTTPException(status_code=500, detail=str(e))
|
| 660 |
|
| 661 |
-
@app.post("/predict/payout_forecast", response_model=PayoutForecastOutput
|
| 662 |
def predict_payout(data: PayoutForecastInput):
|
| 663 |
-
""
|
| 664 |
-
Predicts the estimated influencer payout for the next 30 days
|
| 665 |
-
based on the total budget of a manager's active campaigns.
|
| 666 |
-
"""
|
| 667 |
-
print(f"\n✅ Received request on /predict/payout_forecast")
|
| 668 |
-
if not _payout_forecaster:
|
| 669 |
-
raise HTTPException(status_code=503, detail="Model is not available. Please train the payout forecaster model first.")
|
| 670 |
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
# Ensure the prediction is never negative
|
| 679 |
-
forecasted_amount = max(0, float(prediction))
|
| 680 |
-
|
| 681 |
-
print(f" - ✅ Generated payout forecast: {forecasted_amount}")
|
| 682 |
-
return {
|
| 683 |
-
"forecastedAmount": forecasted_amount,
|
| 684 |
-
"commentary": "Based on the total budget of your current active campaigns."
|
| 685 |
-
}
|
| 686 |
-
|
| 687 |
-
except Exception as e:
|
| 688 |
-
print(f"🚨 An error occurred in /predict/payout_forecast endpoint:")
|
| 689 |
-
traceback.print_exc()
|
| 690 |
-
raise HTTPException(status_code=500, detail=f"An error occurred during prediction: {str(e)}")
|
| 691 |
|
| 692 |
|
| 693 |
@app.post("/analyze/content_quality", response_model=ContentQualityResponse, summary="Analyzes a caption for a quality score")
|
|
@@ -838,58 +842,29 @@ async def caption_assistant_route(request: CaptionAssistRequest):
|
|
| 838 |
raise HTTPException(status_code=500, detail=str(e))
|
| 839 |
|
| 840 |
|
| 841 |
-
@app.post("/predict/campaign-outcome", response_model=ForecastResponse
|
| 842 |
async def predict_campaign_outcome(request: ForecastRequest):
|
| 843 |
-
""
|
| 844 |
-
Takes campaign and influencer stats and uses ML models to predict
|
| 845 |
-
performance (reach, engagement) and potential earnings.
|
| 846 |
-
"""
|
| 847 |
-
print(f"\n✅ Received request on /predict/campaign-outcome")
|
| 848 |
-
|
| 849 |
-
if not _performance_predictor or not _payout_forecaster:
|
| 850 |
-
raise HTTPException(status_code=503, detail="Forecasting models are not available.")
|
| 851 |
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
# Column names MUST match the training script's columns.
|
| 855 |
-
input_data = pd.DataFrame([{
|
| 856 |
-
'budget': request.budget,
|
| 857 |
-
'category': request.category,
|
| 858 |
-
'influencer_count': 1,
|
| 859 |
-
'platform': 'instagram',
|
| 860 |
-
'location': 'USA',
|
| 861 |
-
'followers': request.follower_count,
|
| 862 |
-
'engagement_rate': request.engagement_rate
|
| 863 |
-
}])
|
| 864 |
-
|
| 865 |
-
# --- Performance Prediction ---
|
| 866 |
-
print(" - Predicting performance...")
|
| 867 |
-
# ✅ THE FIX: Pass the columns the model ACTUALLY needs.
|
| 868 |
-
performance_model_cols = ['budget', 'influencer_count', 'platform', 'location', 'category']
|
| 869 |
-
reach_prediction = _performance_predictor.predict(input_data[performance_model_cols])[0]
|
| 870 |
-
engagement_prediction = request.engagement_rate * 100
|
| 871 |
-
|
| 872 |
-
perf_forecast = PerformanceForecast(
|
| 873 |
-
predicted_reach=int(reach_prediction),
|
| 874 |
-
predicted_engagement_rate=round(engagement_prediction, 2)
|
| 875 |
-
)
|
| 876 |
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
payout_prediction = _payout_forecaster.predict(input_data[['budget']])[0]
|
| 881 |
-
|
| 882 |
-
payout_forecast = PayoutForecast(
|
| 883 |
-
estimated_earning=max(0, float(payout_prediction))
|
| 884 |
-
)
|
| 885 |
|
| 886 |
-
|
| 887 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
|
| 894 |
@app.post("/ai/summarize/influencer-analytics", response_model=InfluencerAnalyticsSummaryResponse, summary="Generates a summary for the influencer's analytics page")
|
| 895 |
async def summarize_influencer_analytics(request: InfluencerKpiData):
|
|
@@ -1643,3 +1618,23 @@ def finalize_script_endpoint(request: FinalizeScriptRequest):
|
|
| 1643 |
print(f"🚨 Finalize Script Error: {e}")
|
| 1644 |
traceback.print_exc()
|
| 1645 |
raise HTTPException(status_code=500, detail="Failed to generate the final plan.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
class ChatResponsePayload(BaseModel): response: str
|
| 83 |
class CaptionRequest(BaseModel): caption: str; action: str
|
| 84 |
class CaptionResponse(BaseModel): new_caption: str
|
| 85 |
+
class BudgetRequest(BaseModel):
|
| 86 |
+
campaign_goal: str; influencer_count: int; platform: str; location: str; category: str; final_reach: int
|
| 87 |
+
config: Optional[Dict[str, str]] = None
|
| 88 |
class BudgetResponse(BaseModel): predicted_budget_usd: float
|
| 89 |
class MatcherRequest(BaseModel): campaign_description: str; target_audience_age: str; target_audience_gender: str; engagement_rate: float; followers: int; country: str; niche: str
|
| 90 |
class MatcherResponse(BaseModel): suggested_influencer_ids: List[int]
|
| 91 |
+
class PerformanceRequest(BaseModel):
|
| 92 |
+
budget_usd: float; influencer_count: int; platform: str; location: str; category: str; budget: float
|
| 93 |
+
config: Optional[Dict[str, str]] = None
|
| 94 |
class PerformanceResponse(BaseModel): predicted_engagement_rate: float; predicted_reach: int
|
| 95 |
class StrategyRequest(BaseModel): prompt: str
|
| 96 |
class StrategyResponse(BaseModel): response: str
|
|
|
|
| 125 |
class RankCampaignsResponse(BaseModel): ranked_campaigns: List[RankedCampaignResult]
|
| 126 |
class CaptionAssistRequest(BaseModel): caption: str; action: str = Field(..., description="Action to perform: 'improve', 'hashtags', or 'check_guidelines'"); guidelines: Optional[str] = None
|
| 127 |
class CaptionAssistResponse(BaseModel): new_text: str
|
| 128 |
+
class ForecastRequest(BaseModel):
|
| 129 |
+
budget: float; category: str; follower_count: int; engagement_rate: float
|
| 130 |
+
config: Optional[Dict[str, str]] = None
|
| 131 |
class PerformanceForecast(BaseModel): predicted_engagement_rate: float; predicted_reach: int
|
| 132 |
+
class PayoutForecastInput(BaseModel):
|
| 133 |
+
total_budget_active_campaigns: float
|
| 134 |
+
config: Optional[Dict[str, str]] = None
|
| 135 |
class ForecastResponse(BaseModel): performance: PerformanceForecast; payout: PayoutForecast
|
| 136 |
class InfluencerKpiData(BaseModel): totalReach: int; totalLikes: int; totalComments: int; avgEngagementRate: float; totalSubmissions: int
|
| 137 |
class InfluencerAnalyticsSummaryResponse(BaseModel): summary: str
|
|
|
|
| 171 |
class WeeklyPlanRequest(BaseModel): context: WeeklyPlanContext
|
| 172 |
class PlanOption(BaseModel): type: str; title: str; platform: str; contentType: str; instructions: str; reasoning: str
|
| 173 |
class WeeklyPlanResponse(BaseModel): options: List[PlanOption]
|
| 174 |
+
class RequestConfig(BaseModel):
|
| 175 |
+
model_name: Optional[str] = "phi-2"
|
| 176 |
+
temperature: Optional[float] = 0.7
|
| 177 |
+
system_prompt: Optional[str] = None
|
| 178 |
+
|
| 179 |
+
class DirectPromptPayload(BaseModel):
|
| 180 |
+
prompt: str
|
| 181 |
+
config: Optional[RequestConfig] = None
|
| 182 |
|
| 183 |
# --- FastAPI App ---
|
| 184 |
app = FastAPI(title="Reachify AI Service (Deploy-Ready)", version="11.0.0")
|
|
|
|
| 411 |
except Exception as e:
|
| 412 |
raise HTTPException(status_code=500, detail=f"An internal error occurred in the AI model: {e}")
|
| 413 |
|
| 414 |
+
@app.post("/api/v1/predict/budget", response_model=BudgetResponse)
|
| 415 |
async def predict_budget(request: BudgetRequest):
|
| 416 |
+
if not _budget_predictor: raise HTTPException(status_code=503, detail="Predictor Unavailable")
|
| 417 |
+
|
| 418 |
+
input_data = pd.DataFrame([request.model_dump(exclude={'config'})])
|
| 419 |
+
prediction = float(_budget_predictor.predict(input_data)[0])
|
| 420 |
+
|
| 421 |
+
# ⚙️ CONTROL: Admin Multiplier Check
|
| 422 |
+
if request.config:
|
| 423 |
+
multiplier = float(request.config.get("budget_multiplier", 1.0))
|
| 424 |
+
prediction = prediction * multiplier
|
| 425 |
+
|
| 426 |
return BudgetResponse(predicted_budget_usd=round(prediction, 2))
|
| 427 |
|
| 428 |
@app.post("/api/v1/match/influencers", response_model=MatcherResponse, summary="Match Influencers to Campaign")
|
|
|
|
| 681 |
print(f"🚨 An error occurred in /strategist/generate-weekly-summary: {e}")
|
| 682 |
raise HTTPException(status_code=500, detail=str(e))
|
| 683 |
|
| 684 |
+
@app.post("/predict/payout_forecast", response_model=PayoutForecastOutput)
|
| 685 |
def predict_payout(data: PayoutForecastInput):
|
| 686 |
+
if not _payout_forecaster: raise HTTPException(status_code=503, detail="Model Unavailable")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
|
| 688 |
+
pred = float(_payout_forecaster.predict(pd.DataFrame([{'budget': data.total_budget_active_campaigns}]))[0])
|
| 689 |
+
|
| 690 |
+
# ⚙️ CONTROL
|
| 691 |
+
if data.config:
|
| 692 |
+
pred = pred * float(data.config.get("budget_multiplier", 1.0))
|
| 693 |
+
|
| 694 |
+
return {"forecastedAmount": max(0, pred), "commentary": "Based on budget trends."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
|
| 696 |
|
| 697 |
@app.post("/analyze/content_quality", response_model=ContentQualityResponse, summary="Analyzes a caption for a quality score")
|
|
|
|
| 842 |
raise HTTPException(status_code=500, detail=str(e))
|
| 843 |
|
| 844 |
|
| 845 |
+
@app.post("/predict/campaign-outcome", response_model=ForecastResponse)
|
| 846 |
async def predict_campaign_outcome(request: ForecastRequest):
|
| 847 |
+
if not _performance_predictor or not _payout_forecaster: raise HTTPException(status_code=503, detail="Models Unavailable")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
|
| 849 |
+
input_df = pd.DataFrame([request.model_dump(exclude={'config'})])
|
| 850 |
+
input_df['influencer_count'] = 1; input_df['platform'] = 'instagram'; input_df['location'] = 'USA'; input_df['followers'] = request.follower_count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
|
| 852 |
+
# Predict
|
| 853 |
+
reach = _performance_predictor.predict(input_df[['budget','influencer_count','platform','location','category']])[0]
|
| 854 |
+
payout = float(_payout_forecaster.predict(input_df[['budget']])[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
|
| 856 |
+
# ⚙️ CONTROL: Adjust Values if needed
|
| 857 |
+
if request.config:
|
| 858 |
+
payout_multiplier = float(request.config.get("budget_multiplier", 1.0)) # Shared Logic for simplicity
|
| 859 |
+
payout = payout * payout_multiplier
|
| 860 |
+
# Ensure Minimum Payout (Floor)
|
| 861 |
+
min_payout = float(request.config.get("ml_payout_floor", 0))
|
| 862 |
+
payout = max(min_payout, payout)
|
| 863 |
|
| 864 |
+
return ForecastResponse(
|
| 865 |
+
performance=PerformanceForecast(predicted_reach=int(reach), predicted_engagement_rate=round(request.engagement_rate*100, 2)),
|
| 866 |
+
payout=PayoutForecast(estimated_earning=max(0, payout))
|
| 867 |
+
)
|
| 868 |
|
| 869 |
@app.post("/ai/summarize/influencer-analytics", response_model=InfluencerAnalyticsSummaryResponse, summary="Generates a summary for the influencer's analytics page")
|
| 870 |
async def summarize_influencer_analytics(request: InfluencerKpiData):
|
|
|
|
| 1618 |
print(f"🚨 Finalize Script Error: {e}")
|
| 1619 |
traceback.print_exc()
|
| 1620 |
raise HTTPException(status_code=500, detail="Failed to generate the final plan.")
|
| 1621 |
+
|
| 1622 |
+
|
| 1623 |
+
@app.post("/api/v1/generate-campaign-from-prompt")
|
| 1624 |
+
def create_campaign_from_prompt_endpoint(payload: DirectPromptPayload):
|
| 1625 |
+
# Check if Strategist is loaded
|
| 1626 |
+
if not _ai_strategist:
|
| 1627 |
+
raise HTTPException(status_code=503, detail="AI Strategist model unavailable.")
|
| 1628 |
+
|
| 1629 |
+
# Use Config or Default
|
| 1630 |
+
current_config = payload.config if payload.config else RequestConfig()
|
| 1631 |
+
|
| 1632 |
+
try:
|
| 1633 |
+
# Core Logic Call (Make sure Core Logic updated too)
|
| 1634 |
+
response_text = _ai_strategist.generate_strategy_from_prompt(
|
| 1635 |
+
user_prompt=payload.prompt,
|
| 1636 |
+
config=current_config
|
| 1637 |
+
)
|
| 1638 |
+
return {"response": response_text}
|
| 1639 |
+
except Exception as e:
|
| 1640 |
+
raise HTTPException(status_code=500, detail=str(e))
|
core/strategist.py
CHANGED
|
@@ -64,25 +64,42 @@ class AIStrategist:
|
|
| 64 |
print(f"--- Strategist Skill MOCK ERROR: {e}")
|
| 65 |
return {"error": "An internal error occurred in the mock data generator."}
|
| 66 |
|
| 67 |
-
|
|
|
|
| 68 |
"""
|
| 69 |
-
Generates a
|
| 70 |
"""
|
| 71 |
-
print(f"--- Strategist Skill (General): Received prompt:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
try:
|
|
|
|
| 73 |
response = self.llm(
|
| 74 |
-
|
| 75 |
max_tokens=750,
|
| 76 |
-
temperature=
|
| 77 |
stop=["User:", "Client:", "System:"],
|
| 78 |
)
|
|
|
|
| 79 |
generated_text = response['choices'][0]['text'].strip()
|
| 80 |
-
print("--- Strategist Skill (General):
|
| 81 |
return generated_text
|
|
|
|
| 82 |
except Exception as e:
|
| 83 |
print(f"--- Strategist Skill (General) ERROR: {e}")
|
| 84 |
traceback.print_exc()
|
| 85 |
return "An error occurred in the AI model while generating the strategy."
|
|
|
|
| 86 |
|
| 87 |
def generate_weekly_summary(self, metrics: Dict[str, Any]) -> str:
|
| 88 |
"""
|
|
|
|
| 64 |
print(f"--- Strategist Skill MOCK ERROR: {e}")
|
| 65 |
return {"error": "An internal error occurred in the mock data generator."}
|
| 66 |
|
| 67 |
+
|
| 68 |
+
def generate_strategy_from_prompt(self, user_prompt: str, config=None) -> str:
|
| 69 |
"""
|
| 70 |
+
Generates a strategy using Dynamic Config from Backend.
|
| 71 |
"""
|
| 72 |
+
print(f"--- Strategist Skill (General): Received prompt. Config: {config}")
|
| 73 |
+
|
| 74 |
+
# 1. Temperature Setup (Default 0.75 if config missing)
|
| 75 |
+
temp = config.temperature if config else 0.75
|
| 76 |
+
|
| 77 |
+
# 2. System Prompt Logic
|
| 78 |
+
final_prompt = user_prompt
|
| 79 |
+
|
| 80 |
+
# Agar admin ne koi 'System Prompt' set kiya hai (Jaise "Be concise"), to use jod do
|
| 81 |
+
if config and config.system_prompt:
|
| 82 |
+
# Llama model format: [SYSTEM] instruction [USER] input
|
| 83 |
+
final_prompt = f"[SYSTEM]\n{config.system_prompt}\n\n[USER]\n{user_prompt}"
|
| 84 |
+
|
| 85 |
try:
|
| 86 |
+
# 3. Call Llama with DYNAMIC Temp
|
| 87 |
response = self.llm(
|
| 88 |
+
final_prompt,
|
| 89 |
max_tokens=750,
|
| 90 |
+
temperature=temp, # ✅ Magic yahan hai!
|
| 91 |
stop=["User:", "Client:", "System:"],
|
| 92 |
)
|
| 93 |
+
|
| 94 |
generated_text = response['choices'][0]['text'].strip()
|
| 95 |
+
print("--- Strategist Skill (General): Response Generated.")
|
| 96 |
return generated_text
|
| 97 |
+
|
| 98 |
except Exception as e:
|
| 99 |
print(f"--- Strategist Skill (General) ERROR: {e}")
|
| 100 |
traceback.print_exc()
|
| 101 |
return "An error occurred in the AI model while generating the strategy."
|
| 102 |
+
|
| 103 |
|
| 104 |
def generate_weekly_summary(self, metrics: Dict[str, Any]) -> str:
|
| 105 |
"""
|