Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions DASHBOARD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Social Media Manager Dashboard

A beautiful, modern dashboard for generating social media posts using Mistral AI.

## Quick Start

1. **Install dependencies:**
```bash
uv pip install -r requirements.txt
```

2. **Get your Mistral API Key:**
- Sign up at [Mistral AI](https://console.mistral.ai/)
- Generate an API key from your dashboard

3. **Start the server:**
```bash
python run_server.py
```

4. **Open the dashboard:**
Navigate to `http://localhost:8000` in your browser

## Features

- 🎨 **Modern UI** - Clean, responsive design with dark theme
- ⚡ **Fast Generation** - Powered by Mistral AI models
- 🔒 **Secure** - API key stored locally in browser
- 📋 **Easy Copy** - One-click copy to clipboard
- 🎯 **Model Selection** - Choose between Small, Medium, or Large models
- 💾 **Auto-save** - Your API key is remembered across sessions

## Usage

1. Enter your Mistral API key (stored securely in your browser)
2. Describe the social media post you want to create
3. Select the AI model (Small is fastest, Large is highest quality)
4. Click "Generate Post" and wait for the magic ✨
5. Copy the result and use it on your social media platforms!

## API Endpoints

- `GET /` - Dashboard UI
- `GET /health` - Health check
- `POST /generate-post` - Generate social media post

## Keyboard Shortcuts

- `Ctrl/Cmd + Enter` - Submit form and generate post

## Development

The dashboard consists of:
- **Backend**: FastAPI server ([src/social_media_backend.py](src/social_media_backend.py))
- **Frontend**: HTML/CSS/JavaScript ([src/static/](src/static/))
- **Launcher**: Quick start script ([run_server.py](run_server.py))

## Troubleshooting

**Port already in use?**
Edit [run_server.py](run_server.py) and change the port number.

**API key not working?**
Make sure you're using a valid Mistral API key from https://console.mistral.ai/

**Can't connect to server?**
Check that the server is running and visit `http://localhost:8000`
18 changes: 18 additions & 0 deletions run_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Start the AgentOps Guardian social media manager server."""

import os
import uvicorn

if __name__ == "__main__":
# Read configuration from environment variables with safer defaults
host = os.getenv("BIND_HOST", "127.0.0.1")
port = int(os.getenv("BIND_PORT", "8000"))
reload = os.getenv("BIND_RELOAD", "false").lower() in ("true", "1", "yes")

uvicorn.run(
"src.social_media_backend:app",
host=host,
port=port,
reload=reload,
log_level="info"
)
102 changes: 81 additions & 21 deletions src/social_media_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

import requests
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from pathlib import Path

MISTRAL_CHAT_COMPLETIONS_URL = "https://api.mistral.ai/v1/chat/completions"

app = FastAPI(title="Social Media Manager Backend")

# Get the directory where this file is located
BASE_DIR = Path(__file__).resolve().parent

# Mount static files
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")


class GeneratePostRequest(BaseModel):
"""Request payload for social media post generation."""
Expand All @@ -17,6 +26,12 @@ class GeneratePostRequest(BaseModel):
model: str = "mistral-small-latest"


@app.get("/")
def index():
"""Serve the frontend dashboard."""
return FileResponse(str(BASE_DIR / "static" / "index.html"))


@app.get("/health")
def health() -> dict:
"""Health endpoint for quick service checks."""
Expand All @@ -26,25 +41,70 @@ def health() -> dict:
@app.post("/generate-post")
def generate_post(payload: GeneratePostRequest) -> dict:
"""Generate a social media post from a user prompt using Mistral."""
response = requests.post(
MISTRAL_CHAT_COMPLETIONS_URL,
headers={
"Authorization": f"Bearer {payload.api_key}",
"Content-Type": "application/json",
},
json={
try:
# Clean the API key (remove any whitespace)
clean_api_key = payload.api_key.strip()

# Log key format for debugging (first/last 4 chars only)
key_preview = f"{clean_api_key[:4]}...{clean_api_key[-4:]}" if len(clean_api_key) > 8 else "***"
print(f"DEBUG: Using API key: {key_preview}, length: {len(clean_api_key)}")
Comment on lines +48 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove API-key fingerprint logging from request path.

Lines 48-50 log key preview and length. Secret-derived values should not be emitted to logs.

Suggested fix
-        # Log key format for debugging (first/last 4 chars only)
-        key_preview = f"{clean_api_key[:4]}...{clean_api_key[-4:]}" if len(clean_api_key) > 8 else "***"
-        print(f"DEBUG: Using API key: {key_preview}, length: {len(clean_api_key)}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Log key format for debugging (first/last 4 chars only)
key_preview = f"{clean_api_key[:4]}...{clean_api_key[-4:]}" if len(clean_api_key) > 8 else "***"
print(f"DEBUG: Using API key: {key_preview}, length: {len(clean_api_key)}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social_media_backend.py` around lines 48 - 50, Remove the debugging print
that exposes API key fragments and length: delete or replace the lines
constructing key_preview and the print statement that references clean_api_key,
key_preview, or len(clean_api_key) so no secret-derived value is logged; if a
debug marker is needed use a constant placeholder (e.g., "<REDACTED_API_KEY>")
or log only non-secret request metadata instead; update any references to
key_preview in the surrounding function (e.g., where clean_api_key is processed)
to avoid using the removed variable.


response = requests.post(
MISTRAL_CHAT_COMPLETIONS_URL,
headers={
"Authorization": f"Bearer {clean_api_key}",
"Content-Type": "application/json",
},
json={
"model": payload.model,
"messages": [{"role": "user", "content": payload.prompt}],
},
timeout=60,
)

# Handle API errors with helpful messages
if response.status_code == 401:
from fastapi import HTTPException
error_msg = response.json().get("message", "") if response.text else ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/social_media_backend.py | sed -n '50,100p'

Repository: mitanuriel/agentSecOps_Guardian

Length of output: 2420


Safely parse non-JSON error responses before building HTTP errors.

Lines 68 and 82 call response.json() directly without handling potential json.JSONDecodeError. If the upstream API returns an error response with a non-JSON body, these calls will raise an unhandled exception, bypassing your intended HTTPException handling. Line 68's check of response.text only verifies content exists, not that it's valid JSON. Line 82 has no protection whatsoever.

Create a helper function to safely parse responses:

Suggested fix
+def _safe_json(response: requests.Response) -> dict:
+    try:
+        return response.json()
+    except ValueError:
+        return {}
+
         if response.status_code == 401:
             from fastapi import HTTPException
-            error_msg = response.json().get("message", "") if response.text else ""
+            body = _safe_json(response)
+            error_msg = body.get("message", "") if response.text else ""
             print(f"DEBUG: 401 error details - {error_msg}")
         elif response.status_code >= 400:
             from fastapi import HTTPException
-            error_detail = response.json().get("error", {}).get("message", "Unknown error")
+            body = _safe_json(response)
+            error_detail = body.get("error", {}).get("message", "Unknown error")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social_media_backend.py` at line 68, Create a small helper function
(e.g., safe_parse_json(response)) that attempts response.json() and catches
json.JSONDecodeError (and ValueError for some clients), returning the parsed
dict on success or None on failure; then replace direct response.json() calls
used to build error_msg (the assignment currently using
response.json().get("message", "") if response.text else "") and the later
unprotected response.json() usage with the helper, falling back to response.text
or an empty string when safe_parse_json returns None, so HTTPException
construction always uses a string and never raises on non-JSON bodies.

print(f"DEBUG: 401 error details - {error_msg}")
raise HTTPException(
status_code=401,
detail=f"Invalid API key. Check: 1) Key is correctly copied from Mistral console 2) No extra spaces 3) Key is activated. API response: {error_msg}"
)
elif response.status_code == 429:
from fastapi import HTTPException
raise HTTPException(
status_code=429,
detail="Rate limit exceeded. Please wait a moment and try again."
)
elif response.status_code >= 400:
from fastapi import HTTPException
error_detail = response.json().get("error", {}).get("message", "Unknown error")
raise HTTPException(
status_code=response.status_code,
detail=f"Mistral API error: {error_detail}"
)

response.raise_for_status()

completion = response.json()
post_text = completion["choices"][0]["message"]["content"]

return {
"post": post_text,
"model": payload.model,
"messages": [{"role": "user", "content": payload.prompt}],
},
timeout=60,
)
response.raise_for_status()

completion = response.json()
post_text = completion["choices"][0]["message"]["content"]

return {
"post": post_text,
"model": payload.model,
"provider": "mistral",
}
"provider": "mistral",
}

except requests.exceptions.Timeout:
from fastapi import HTTPException
raise HTTPException(
status_code=504,
detail="Request timeout. The API took too long to respond. Please try again."
)
except requests.exceptions.RequestException as e:
from fastapi import HTTPException
raise HTTPException(
status_code=500,
detail=f"Network error: {str(e)}"
)
Comment on lines 41 to +110
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CI is currently blocked by formatting in this file.

Pipeline reports black --check src tests failing with this file needing reformatting. Please run Black and commit the formatted output to unblock merge.

🧰 Tools
🪛 Ruff (0.15.2)

[warning] 93-97: Consider moving this statement to an else block

(TRY300)


[warning] 101-104: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 107-110: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 109-109: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social_media_backend.py` around lines 41 - 110, The file fails CI because
it is not formatted to Black's expectations; run Black (e.g., black
src/social_media_backend.py or black src tests) to reformat the file, verify
generate_post and its surrounding code are reformatted (PEP8/Black style), stage
and commit the changes, and push so the pipeline's `black --check src tests`
passes.

Loading
Loading