Skip to content

Commit 94abc4b

Browse files
authored
Merge pull request #29 from chintakjoshi/anthropic2nvidianim
switching from Anthropic SDK to Nvidia NIM
2 parents 4780e63 + bfac8b6 commit 94abc4b

10 files changed

Lines changed: 211 additions & 106 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Highlights:
2222
## Tech Stack
2323

2424
- Frontend: React + TypeScript + Vite + Tailwind CSS
25-
- Backend: Flask + SQLAlchemy + Alembic + Pydantic + Anthropic SDK
25+
- Backend: Flask + SQLAlchemy + Alembic + Pydantic + OpenAI SDK (for NVIDIA NIM)
2626
- Database: PostgreSQL
2727

2828
## Prerequisites
@@ -42,12 +42,12 @@ copy .env.example .env
4242
```
4343

4444
Required for AI replies:
45-
- Set `ANTHROPIC_API_KEY` in `backend/.env`
45+
- Set `NIM_API_KEY` in `backend/.env`
4646

4747
Key backend env values:
4848
- `DATABASE_URL`
4949
- `CORS_ORIGINS`
50-
- `ANTHROPIC_MODEL`
50+
- `NIM_MODEL`
5151
- `WEB_SEARCH_MAX_RESULTS`
5252

5353
### Frontend env

backend/.env.example

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ FLASK_ENV=development
33
FLASK_DEBUG=1
44
CORS_ORIGINS=http://localhost:5173
55
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/manuscriptly_writer
6-
ANTHROPIC_API_KEY=your_anthropic_api_key
7-
ANTHROPIC_MODEL=claude-haiku-4-5-20251001
8-
ANTHROPIC_MAX_TOKENS=4000
9-
ANTHROPIC_TEMPERATURE=0.4
10-
ANTHROPIC_MAX_TOOL_ITERATIONS=5
11-
ANTHROPIC_RETRY_MAX_ATTEMPTS=3
12-
ANTHROPIC_RETRY_BASE_DELAY_SECONDS=0.75
13-
ANTHROPIC_RETRY_MAX_DELAY_SECONDS=4.0
6+
NIM_API_KEY=your_nvidia_nim_api_key
7+
NIM_BASE_URL=https://integrate.api.nvidia.com/v1
8+
NIM_MODEL=openai/gpt-oss-120b
9+
AI_MAX_TOKENS=4000
10+
AI_TEMPERATURE=0.4
11+
AI_MAX_TOOL_ITERATIONS=5
12+
AI_RETRY_MAX_ATTEMPTS=3
13+
AI_RETRY_BASE_DELAY_SECONDS=0.75
14+
AI_RETRY_MAX_DELAY_SECONDS=4.0
1415
WEB_SEARCH_API_URL=https://api.duckduckgo.com/
1516
WEB_SEARCH_TIMEOUT_SECONDS=8.0
1617
WEB_SEARCH_MAX_RESULTS=5

backend/app/agent_tools/handlers.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from time import perf_counter
66
from typing import Any
77

8-
from anthropic import Anthropic
9-
from anthropic import APIConnectionError, APIError, APITimeoutError
8+
from openai import APIConnectionError, APIError, APITimeoutError, OpenAI
109
from sqlalchemy.orm import Session
1110

1211
from app.agent_tools.schemas import CreateContentIdeaInput, ExecutePlanInput, UpdateContentPlanInput, WebSearchInput
@@ -28,6 +27,13 @@ class ToolValidationError(ToolHandlerError):
2827
pass
2928

3029

30+
def _get_ai_client() -> OpenAI | None:
31+
api_key = Config.NIM_API_KEY.strip()
32+
if not api_key or api_key == "your_nvidia_nim_api_key":
33+
return None
34+
return OpenAI(api_key=api_key, base_url=Config.NIM_BASE_URL)
35+
36+
3137
def handle_create_content_idea(payload: CreateContentIdeaInput) -> dict[str, Any]:
3238
return _run_tool("create_content_idea", payload, _create_content_idea)
3339

@@ -260,8 +266,8 @@ def _generate_plan_with_ai(
260266
constraints: dict[str, Any] | None,
261267
user_context: dict[str, Any],
262268
) -> dict[str, Any] | None:
263-
api_key = Config.ANTHROPIC_API_KEY.strip()
264-
if not api_key or api_key == "your_anthropic_api_key":
269+
client = _get_ai_client()
270+
if client is None:
265271
return None
266272

267273
prompt = (
@@ -277,13 +283,14 @@ def _generate_plan_with_ai(
277283
)
278284

279285
try:
280-
client = Anthropic(api_key=api_key)
281-
response = client.messages.create(
282-
model=Config.ANTHROPIC_MODEL,
283-
max_tokens=Config.ANTHROPIC_MAX_TOKENS,
284-
temperature=Config.ANTHROPIC_TEMPERATURE,
285-
system="You are a precise content strategist. Follow output format exactly.",
286-
messages=[{"role": "user", "content": prompt}],
286+
response = client.chat.completions.create(
287+
model=Config.NIM_MODEL,
288+
max_tokens=Config.AI_MAX_TOKENS,
289+
temperature=Config.AI_TEMPERATURE,
290+
messages=[
291+
{"role": "system", "content": "You are a precise content strategist. Follow output format exactly."},
292+
{"role": "user", "content": prompt},
293+
],
287294
)
288295
text = _extract_text_from_response(response)
289296
return _parse_json_from_text(text)
@@ -297,8 +304,8 @@ def _generate_blog_with_ai(
297304
output_format: str,
298305
user_context: dict[str, Any],
299306
) -> dict[str, Any] | None:
300-
api_key = Config.ANTHROPIC_API_KEY.strip()
301-
if not api_key or api_key == "your_anthropic_api_key":
307+
client = _get_ai_client()
308+
if client is None:
302309
return None
303310

304311
prompt = (
@@ -316,13 +323,14 @@ def _generate_blog_with_ai(
316323
)
317324

318325
try:
319-
client = Anthropic(api_key=api_key)
320-
response = client.messages.create(
321-
model=Config.ANTHROPIC_MODEL,
322-
max_tokens=Config.ANTHROPIC_MAX_TOKENS,
323-
temperature=Config.ANTHROPIC_TEMPERATURE,
324-
system="You are a senior blog writer. Follow output format exactly.",
325-
messages=[{"role": "user", "content": prompt}],
326+
response = client.chat.completions.create(
327+
model=Config.NIM_MODEL,
328+
max_tokens=Config.AI_MAX_TOKENS,
329+
temperature=Config.AI_TEMPERATURE,
330+
messages=[
331+
{"role": "system", "content": "You are a senior blog writer. Follow output format exactly."},
332+
{"role": "user", "content": prompt},
333+
],
326334
)
327335
text = _extract_text_from_response(response)
328336
parsed = _coerce_blog_payload_from_text(text, plan)
@@ -343,7 +351,7 @@ def _generate_blog_with_ai(
343351

344352

345353
def _generate_blog_markdown_retry_with_ai(
346-
client: Anthropic,
354+
client: OpenAI,
347355
plan: ContentPlan,
348356
writing_instructions: str | None,
349357
user_context: dict[str, Any],
@@ -360,12 +368,14 @@ def _generate_blog_markdown_retry_with_ai(
360368
f"User context (JSON):\n{json.dumps(user_context, ensure_ascii=True)}"
361369
)
362370
try:
363-
response = client.messages.create(
364-
model=Config.ANTHROPIC_MODEL,
365-
max_tokens=Config.ANTHROPIC_MAX_TOKENS,
366-
temperature=Config.ANTHROPIC_TEMPERATURE,
367-
system="You are a senior blog writer. Return markdown only.",
368-
messages=[{"role": "user", "content": prompt}],
371+
response = client.chat.completions.create(
372+
model=Config.NIM_MODEL,
373+
max_tokens=Config.AI_MAX_TOKENS,
374+
temperature=Config.AI_TEMPERATURE,
375+
messages=[
376+
{"role": "system", "content": "You are a senior blog writer. Return markdown only."},
377+
{"role": "user", "content": prompt},
378+
],
369379
)
370380
return _extract_text_from_response(response)
371381
except (APIError, APIConnectionError, APITimeoutError, ToolHandlerError):
@@ -436,6 +446,13 @@ def _generate_blog_fallback(
436446

437447

438448
def _extract_text_from_response(response: Any) -> str:
449+
choices = getattr(response, "choices", None)
450+
if isinstance(choices, list) and choices:
451+
message = getattr(choices[0], "message", None)
452+
text = (getattr(message, "content", None) or "").strip() if message is not None else ""
453+
if text:
454+
return text
455+
439456
text_parts: list[str] = []
440457
for block in getattr(response, "content", []):
441458
if getattr(block, "type", None) == "text":

backend/app/agent_tools/registry.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ def to_anthropic_tool(self) -> dict[str, Any]:
3434
"input_schema": self.input_model.model_json_schema(),
3535
}
3636

37+
def to_openai_tool(self) -> dict[str, Any]:
38+
return {
39+
"type": "function",
40+
"function": {
41+
"name": self.name,
42+
"description": self.description,
43+
"parameters": self.input_model.model_json_schema(),
44+
},
45+
}
46+
3747

3848
class ToolRegistry:
3949
def __init__(self) -> None:
@@ -55,3 +65,6 @@ def list(self) -> list[ToolDefinition]:
5565

5666
def list_anthropic_tools(self) -> list[dict[str, Any]]:
5767
return [tool.to_anthropic_tool() for tool in self._tools.values()]
68+
69+
def list_openai_tools(self) -> list[dict[str, Any]]:
70+
return [tool.to_openai_tool() for tool in self._tools.values()]

backend/app/api/routes/agent.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
@agent_bp.post("/chat")
2525
def chat_with_agent():
2626
"""
27-
Create user message and generate assistant reply via Anthropic
27+
Create user message and generate assistant reply via AI provider
2828
---
2929
tags:
3030
- Agent
@@ -48,11 +48,11 @@ def chat_with_agent():
4848
schema:
4949
$ref: '#/definitions/ErrorResponse'
5050
500:
51-
description: Anthropic configuration missing.
51+
description: AI provider configuration missing.
5252
schema:
5353
$ref: '#/definitions/ErrorResponse'
5454
502:
55-
description: Anthropic completion failed.
55+
description: AI provider completion failed.
5656
schema:
5757
$ref: '#/definitions/ErrorResponse'
5858
"""
@@ -86,7 +86,7 @@ def chat_with_agent():
8686
sse_manager.publish("message.created", user_payload, session_id=session_id)
8787
sse_manager.publish(
8888
"agent.response.started",
89-
{"conversation_id": session_id, "model": Config.ANTHROPIC_MODEL},
89+
{"conversation_id": session_id, "model": Config.NIM_MODEL},
9090
session_id=session_id,
9191
)
9292

@@ -141,7 +141,7 @@ def emit_agent_event(event_name: str, payload: dict) -> None:
141141
{
142142
"conversation_id": session_id,
143143
"assistant_message_id": assistant_payload["id"],
144-
"model": Config.ANTHROPIC_MODEL,
144+
"model": Config.NIM_MODEL,
145145
},
146146
session_id=session_id,
147147
)
@@ -151,7 +151,7 @@ def emit_agent_event(event_name: str, payload: dict) -> None:
151151
{
152152
"user_message": user_payload,
153153
"assistant_message": assistant_payload,
154-
"model": Config.ANTHROPIC_MODEL,
154+
"model": Config.NIM_MODEL,
155155
}
156156
),
157157
201,

backend/app/api/swagger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@
259259
"properties": {
260260
"user_message": {"$ref": "#/definitions/Message"},
261261
"assistant_message": {"$ref": "#/definitions/Message"},
262-
"model": {"type": "string", "example": "claude-haiku-4-5-20251001"},
262+
"model": {"type": "string", "example": "openai/gpt-oss-120b"},
263263
},
264264
},
265265
"StreamTestRequest": {

backend/app/core/config.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,33 @@ class Config:
1717
"DATABASE_URL",
1818
"postgresql+psycopg://postgres:postgres@localhost:5432/manuscriptly_writer",
1919
)
20-
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
21-
ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001")
22-
ANTHROPIC_MAX_TOKENS = int(os.getenv("ANTHROPIC_MAX_TOKENS", "4000"))
23-
ANTHROPIC_TEMPERATURE = float(os.getenv("ANTHROPIC_TEMPERATURE", "0.4"))
24-
ANTHROPIC_MAX_TOOL_ITERATIONS = int(os.getenv("ANTHROPIC_MAX_TOOL_ITERATIONS", "5"))
25-
ANTHROPIC_RETRY_MAX_ATTEMPTS = int(os.getenv("ANTHROPIC_RETRY_MAX_ATTEMPTS", "3"))
26-
ANTHROPIC_RETRY_BASE_DELAY_SECONDS = float(os.getenv("ANTHROPIC_RETRY_BASE_DELAY_SECONDS", "0.75"))
27-
ANTHROPIC_RETRY_MAX_DELAY_SECONDS = float(os.getenv("ANTHROPIC_RETRY_MAX_DELAY_SECONDS", "4.0"))
20+
NIM_API_KEY = os.getenv("NIM_API_KEY", os.getenv("ANTHROPIC_API_KEY", ""))
21+
NIM_BASE_URL = os.getenv("NIM_BASE_URL", "https://integrate.api.nvidia.com/v1")
22+
NIM_MODEL = os.getenv("NIM_MODEL", os.getenv("ANTHROPIC_MODEL", "openai/gpt-oss-120b"))
23+
AI_MAX_TOKENS = int(os.getenv("AI_MAX_TOKENS", os.getenv("ANTHROPIC_MAX_TOKENS", "4000")))
24+
AI_TEMPERATURE = float(os.getenv("AI_TEMPERATURE", os.getenv("ANTHROPIC_TEMPERATURE", "0.4")))
25+
AI_MAX_TOOL_ITERATIONS = int(
26+
os.getenv("AI_MAX_TOOL_ITERATIONS", os.getenv("ANTHROPIC_MAX_TOOL_ITERATIONS", "5"))
27+
)
28+
AI_RETRY_MAX_ATTEMPTS = int(
29+
os.getenv("AI_RETRY_MAX_ATTEMPTS", os.getenv("ANTHROPIC_RETRY_MAX_ATTEMPTS", "3"))
30+
)
31+
AI_RETRY_BASE_DELAY_SECONDS = float(
32+
os.getenv("AI_RETRY_BASE_DELAY_SECONDS", os.getenv("ANTHROPIC_RETRY_BASE_DELAY_SECONDS", "0.75"))
33+
)
34+
AI_RETRY_MAX_DELAY_SECONDS = float(
35+
os.getenv("AI_RETRY_MAX_DELAY_SECONDS", os.getenv("ANTHROPIC_RETRY_MAX_DELAY_SECONDS", "4.0"))
36+
)
37+
38+
# Backward-compatible aliases for existing call sites/tests.
39+
ANTHROPIC_API_KEY = NIM_API_KEY
40+
ANTHROPIC_MODEL = NIM_MODEL
41+
ANTHROPIC_MAX_TOKENS = AI_MAX_TOKENS
42+
ANTHROPIC_TEMPERATURE = AI_TEMPERATURE
43+
ANTHROPIC_MAX_TOOL_ITERATIONS = AI_MAX_TOOL_ITERATIONS
44+
ANTHROPIC_RETRY_MAX_ATTEMPTS = AI_RETRY_MAX_ATTEMPTS
45+
ANTHROPIC_RETRY_BASE_DELAY_SECONDS = AI_RETRY_BASE_DELAY_SECONDS
46+
ANTHROPIC_RETRY_MAX_DELAY_SECONDS = AI_RETRY_MAX_DELAY_SECONDS
2847
WEB_SEARCH_API_URL = os.getenv("WEB_SEARCH_API_URL", "https://api.duckduckgo.com/")
2948
WEB_SEARCH_TIMEOUT_SECONDS = float(os.getenv("WEB_SEARCH_TIMEOUT_SECONDS", "8.0"))
3049
WEB_SEARCH_MAX_RESULTS = int(os.getenv("WEB_SEARCH_MAX_RESULTS", "5"))

0 commit comments

Comments
 (0)