Spaces:
Running
Running
details.wes commited on
Commit ·
9b7c591
1
Parent(s): 00db958
Deploy agent service
Browse files- Dockerfile +19 -0
- README.md +16 -4
- __pycache__/crew.cpython-313.pyc +0 -0
- __pycache__/service.cpython-313.pyc +0 -0
- config/agents.yaml +43 -0
- config/tasks.yaml +34 -0
- crew.py +97 -0
- requirements.txt +16 -0
- service.py +68 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
COPY requirements.txt /app/requirements.txt
|
| 13 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY . /app
|
| 16 |
+
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
CMD ["sh", "-c", "uvicorn service:app --host 0.0.0.0 --port ${PORT:-7860}"]
|
README.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
| 1 |
---
|
| 2 |
-
title: Automatic Post Agents
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Automatic Post — Agents
|
| 3 |
+
emoji: ✍️
|
| 4 |
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Agents service (CrewAI + NVIDIA NIM)
|
| 12 |
+
|
| 13 |
+
FastAPI app that exposes:
|
| 14 |
+
|
| 15 |
+
- `GET /health` — liveness check
|
| 16 |
+
- `POST /generate` — body: `topic`, optional `feedback`, `memory_context`, `tone_instruction`; returns `{"post": "..."}`
|
| 17 |
+
|
| 18 |
+
Set **`NVIDIA_NIM_API_KEY`** as a [Space secret](https://huggingface.co/docs/hub/spaces-overview#managing-secrets) (required). Optionally set `AGENTS_LLM_MODEL` (default: `nvidia_nim/meta/llama-3.1-70b-instruct`).
|
| 19 |
+
|
| 20 |
+
**Port:** the container listens on `$PORT` when the platform sets it, otherwise **7860** (Hugging Face Spaces default).
|
| 21 |
+
|
| 22 |
+
For local dev without Docker you can still run `uvicorn service:app --port 9000` if you prefer.
|
__pycache__/crew.cpython-313.pyc
ADDED
|
Binary file (4.86 kB). View file
|
|
|
__pycache__/service.cpython-313.pyc
ADDED
|
Binary file (3.31 kB). View file
|
|
|
config/agents.yaml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
research_agent:
|
| 2 |
+
role: Senior Tech News Research Analyst
|
| 3 |
+
goal: >
|
| 4 |
+
Find the absolute latest technology news, product launches, updates, and
|
| 5 |
+
trending topics from 2024-2025 using web search. Always prioritize recency —
|
| 6 |
+
the newer the source, the better. Search multiple times with different queries
|
| 7 |
+
to ensure the findings are current and diverse.
|
| 8 |
+
backstory: >
|
| 9 |
+
You are a senior research analyst at a top tech publication.
|
| 10 |
+
Your job is to surface what's happening RIGHT NOW in the tech world —
|
| 11 |
+
not what happened last year. You run multiple targeted searches,
|
| 12 |
+
cross-check recency, and only report findings from recent sources.
|
| 13 |
+
You never rely on your training knowledge. You always use the search tool
|
| 14 |
+
and cite real URLs and publication names.
|
| 15 |
+
|
| 16 |
+
writer_agent:
|
| 17 |
+
role: LinkedIn Content Writer
|
| 18 |
+
goal: >
|
| 19 |
+
Create engaging, timely LinkedIn posts from fresh research data.
|
| 20 |
+
The post must feel current and reference specific recent events or updates.
|
| 21 |
+
backstory: >
|
| 22 |
+
Skilled LinkedIn creator who writes concise, punchy posts with strong hooks
|
| 23 |
+
and clear CTAs. Specializes in making complex tech news accessible and
|
| 24 |
+
interesting to a professional audience.
|
| 25 |
+
|
| 26 |
+
review_agent:
|
| 27 |
+
role: Content Reviewer
|
| 28 |
+
goal: >
|
| 29 |
+
Ensure the post is accurate, clear, professional, and sounds human.
|
| 30 |
+
Verify that it references the latest findings and does not sound generic.
|
| 31 |
+
backstory: >
|
| 32 |
+
Detail-oriented reviewer who improves readability, logical flow, and
|
| 33 |
+
authenticity. Flags anything that sounds like recycled or outdated content.
|
| 34 |
+
|
| 35 |
+
editor_agent:
|
| 36 |
+
role: LinkedIn Content Editor
|
| 37 |
+
goal: >
|
| 38 |
+
Refine and format the post for maximum LinkedIn engagement.
|
| 39 |
+
Optimize structure, hashtags, and tone for the LinkedIn algorithm.
|
| 40 |
+
backstory: >
|
| 41 |
+
Expert LinkedIn editor who knows how to format posts for reach —
|
| 42 |
+
strong opening line, clear paragraphs, trending hashtags, and
|
| 43 |
+
just enough emojis to feel human without being cringe.
|
config/tasks.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
write_post_task:
|
| 2 |
+
description: >
|
| 3 |
+
Write a LinkedIn post draft about: {topic}
|
| 4 |
+
|
| 5 |
+
The input already includes a small "Research findings" section with recent updates.
|
| 6 |
+
Do NOT browse the web or add citations/URLs beyond what's provided in the input.
|
| 7 |
+
|
| 8 |
+
Rules:
|
| 9 |
+
- Open with a punchy hook referencing something NEW or surprising from the research
|
| 10 |
+
- Mention 3 specific recent updates or facts (with context)
|
| 11 |
+
- Show why this matters RIGHT NOW for professionals
|
| 12 |
+
- End with a thought-provoking question or CTA
|
| 13 |
+
- Max 170 words
|
| 14 |
+
- Write in first-person, conversational tone
|
| 15 |
+
- No corporate jargon
|
| 16 |
+
expected_output: >
|
| 17 |
+
A LinkedIn post draft under 170 words.
|
| 18 |
+
agent: writer_agent
|
| 19 |
+
|
| 20 |
+
edit_post_task:
|
| 21 |
+
description: >
|
| 22 |
+
Polish this LinkedIn post for maximum engagement: {write_post_task.output}
|
| 23 |
+
|
| 24 |
+
Apply these formatting improvements:
|
| 25 |
+
- Make the first line stand alone as a hook (no more than 10 words)
|
| 26 |
+
- Add a blank line between each paragraph or thought
|
| 27 |
+
- Ensure it still references the recent updates from the research input
|
| 28 |
+
- Add 3-5 relevant hashtags at the end
|
| 29 |
+
- Add 0-2 emojis only if they feel natural (optional)
|
| 30 |
+
- Max 200 words total
|
| 31 |
+
- Keep it human, concise, and non-corporate
|
| 32 |
+
expected_output: >
|
| 33 |
+
Final LinkedIn post under 200 words, properly formatted with hashtags.
|
| 34 |
+
agent: editor_agent
|
crew.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crew definition for the agents service.
|
| 3 |
+
|
| 4 |
+
This service is locked to NVIDIA NIM as its only model provider. All requests go
|
| 5 |
+
through LiteLLM's `nvidia_nim/...` provider, which requires `NVIDIA_NIM_API_KEY`.
|
| 6 |
+
|
| 7 |
+
Configuration via env vars:
|
| 8 |
+
|
| 9 |
+
AGENTS_LLM_MODEL e.g. "nvidia_nim/meta/llama-3.1-70b-instruct" (default)
|
| 10 |
+
AGENTS_LLM_BASE_URL defaults to NVIDIA's hosted inference endpoint
|
| 11 |
+
AGENTS_LLM_TEMPERATURE float, defaults to 0.5
|
| 12 |
+
NVIDIA_NIM_API_KEY required (or NVIDIA_API_KEY as an alias)
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
from typing import List
|
| 18 |
+
|
| 19 |
+
from crewai import Agent, Crew, LLM, Process, Task
|
| 20 |
+
from crewai.agents.agent_builder.base_agent import BaseAgent
|
| 21 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_NVIDIA_DEFAULT_BASE = "https://integrate.api.nvidia.com/v1"
|
| 25 |
+
_NVIDIA_DEFAULT_MODEL = "nvidia_nim/meta/llama-3.1-70b-instruct"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _resolve_nvidia_key() -> str:
|
| 29 |
+
"""Return the NVIDIA NIM key, accepting either env var name. Hard-fail if missing."""
|
| 30 |
+
key = (os.getenv("NVIDIA_NIM_API_KEY") or os.getenv("NVIDIA_API_KEY") or "").strip()
|
| 31 |
+
if not key:
|
| 32 |
+
raise RuntimeError(
|
| 33 |
+
"Missing NVIDIA NIM key. Set NVIDIA_NIM_API_KEY (or NVIDIA_API_KEY) in the "
|
| 34 |
+
"agents service environment."
|
| 35 |
+
)
|
| 36 |
+
return key
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _build_llm() -> LLM:
|
| 40 |
+
model = (os.getenv("AGENTS_LLM_MODEL") or _NVIDIA_DEFAULT_MODEL).strip()
|
| 41 |
+
if not model.startswith("nvidia_nim/"):
|
| 42 |
+
raise RuntimeError(
|
| 43 |
+
f"This service only supports NVIDIA NIM models. Got model={model!r}; "
|
| 44 |
+
f"expected a value starting with 'nvidia_nim/'."
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
base_url = (os.getenv("AGENTS_LLM_BASE_URL") or _NVIDIA_DEFAULT_BASE).strip().rstrip("/")
|
| 48 |
+
temperature = float(os.getenv("AGENTS_LLM_TEMPERATURE", "0.5"))
|
| 49 |
+
|
| 50 |
+
nvidia_key = _resolve_nvidia_key()
|
| 51 |
+
os.environ["NVIDIA_NIM_API_KEY"] = nvidia_key
|
| 52 |
+
os.environ["NVIDIA_NIM_API_BASE"] = base_url + "/"
|
| 53 |
+
|
| 54 |
+
return LLM(model=model, base_url=base_url, temperature=temperature)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@CrewBase
|
| 58 |
+
class ContentCrew:
|
| 59 |
+
"""LinkedIn post writing crew — runs inside the agents service."""
|
| 60 |
+
|
| 61 |
+
agents: List[BaseAgent]
|
| 62 |
+
tasks: List[Task]
|
| 63 |
+
|
| 64 |
+
@agent
|
| 65 |
+
def writer_agent(self) -> Agent:
|
| 66 |
+
return Agent(
|
| 67 |
+
config=self.agents_config["writer_agent"],
|
| 68 |
+
llm=_build_llm(),
|
| 69 |
+
max_tokens=420,
|
| 70 |
+
verbose=False,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
@agent
|
| 74 |
+
def editor_agent(self) -> Agent:
|
| 75 |
+
return Agent(
|
| 76 |
+
config=self.agents_config["editor_agent"],
|
| 77 |
+
llm=_build_llm(),
|
| 78 |
+
max_tokens=380,
|
| 79 |
+
verbose=False,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
@task
|
| 83 |
+
def write_post_task(self) -> Task:
|
| 84 |
+
return Task(config=self.tasks_config["write_post_task"])
|
| 85 |
+
|
| 86 |
+
@task
|
| 87 |
+
def edit_post_task(self) -> Task:
|
| 88 |
+
return Task(config=self.tasks_config["edit_post_task"])
|
| 89 |
+
|
| 90 |
+
@crew
|
| 91 |
+
def crew(self) -> Crew:
|
| 92 |
+
return Crew(
|
| 93 |
+
agents=self.agents,
|
| 94 |
+
tasks=self.tasks,
|
| 95 |
+
process=Process.sequential,
|
| 96 |
+
verbose=False,
|
| 97 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
python-dotenv
|
| 4 |
+
|
| 5 |
+
# NVIDIA NIM is the only supported LLM provider.
|
| 6 |
+
# CrewAI calls NIM through LiteLLM under the `nvidia_nim/...` provider.
|
| 7 |
+
crewai
|
| 8 |
+
crewai-tools
|
| 9 |
+
litellm
|
| 10 |
+
|
| 11 |
+
# Transitively required by litellm/crewai for NIM (OpenAI-compatible client + tokenizer).
|
| 12 |
+
openai
|
| 13 |
+
tiktoken
|
| 14 |
+
|
| 15 |
+
httpx
|
| 16 |
+
|
service.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
from fastapi import FastAPI, HTTPException
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from crew import ContentCrew
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# Load agents/.env if present (kept separate from backend/.env).
|
| 14 |
+
# Also load backend/.env to support local dev where keys are stored there.
|
| 15 |
+
_agents_env = Path(__file__).resolve().parent / ".env"
|
| 16 |
+
_backend_env = Path(__file__).resolve().parents[1] / "backend" / ".env"
|
| 17 |
+
load_dotenv(_agents_env, override=False)
|
| 18 |
+
load_dotenv(_backend_env, override=False)
|
| 19 |
+
|
| 20 |
+
app = FastAPI()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class GenerateRequest(BaseModel):
|
| 24 |
+
topic: str
|
| 25 |
+
feedback: str | None = None
|
| 26 |
+
memory_context: str = ""
|
| 27 |
+
tone_instruction: str = ""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@app.get("/health")
|
| 31 |
+
def health():
|
| 32 |
+
return {"ok": True}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.post("/generate")
|
| 36 |
+
def generate(req: GenerateRequest):
|
| 37 |
+
topic = (req.topic or "").strip()
|
| 38 |
+
if not topic:
|
| 39 |
+
raise HTTPException(400, "topic is required")
|
| 40 |
+
|
| 41 |
+
prompt = topic[:4000]
|
| 42 |
+
|
| 43 |
+
if req.memory_context:
|
| 44 |
+
prompt = f"{req.memory_context.strip()}\n\nTopic to write about: {prompt}"
|
| 45 |
+
|
| 46 |
+
if req.tone_instruction:
|
| 47 |
+
prompt = f"{prompt}\n\nTone requirements: {req.tone_instruction.strip()[:1200]}"
|
| 48 |
+
|
| 49 |
+
if req.feedback:
|
| 50 |
+
prompt += f"\n\nPrevious post rejected. Feedback: {req.feedback.strip()[:1000]}"
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
crew_instance = ContentCrew().crew()
|
| 54 |
+
result = crew_instance.kickoff(inputs={"topic": prompt})
|
| 55 |
+
return {"post": str(result)}
|
| 56 |
+
except Exception as exc:
|
| 57 |
+
# Don't leak internal stack traces cross-service.
|
| 58 |
+
msg = str(exc) or "Agent generation failed"
|
| 59 |
+
if os.getenv("AGENTS_DEBUG", "").strip() in ("1", "true", "yes"):
|
| 60 |
+
raise
|
| 61 |
+
raise HTTPException(500, msg[:500])
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
import uvicorn
|
| 66 |
+
|
| 67 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "9000")))
|
| 68 |
+
|