details.wes commited on
Commit
9b7c591
·
1 Parent(s): 00db958

Deploy agent service

Browse files
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: red
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
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
+