
Agnes Ai Generation
- 2k installs
- 376 repo stars
- Updated June 21, 2026
- yacey/agnes-ai-generation-skill
Skill that calls Agnes AI text, image, and video generation models via authenticated API endpoints, returning generated content URLs and streaming responses.
About
Agnes AI Generation skill wraps the Agnes / Sapiens AI generation APIs at apihub.agnes-ai.com, enabling developers to generate and edit text, images, and videos programmatically. It supports text completions with streaming, text-to-image and image-to-image synthesis using agnes-image-2.1-flash, and asynchronous text-to-video, image-to-video, and keyframe animation via agnes-video-v2.0. The Python CLI validates parameters before API calls, handles async video polling, and normalizes streaming responses. Best for teams building AI-driven content workflows, image generation pipelines, and video synthesis applications. Requires an API key and supports smoke-test verification across all modalities.
- Multi-modal generation: text, image (text-to-image, image-to-image), and video (text-to-video, image-to-video, keyframes
- Streaming text responses with normalized content aggregation and raw prefix inspection
- Asynchronous video task creation and polling-based retrieval with seed reproducibility
- Built-in Python CLI (agnes_api.py) with parameter validation, smoke-test suite, and error reporting
- Non-English prompt translation to fluent English for stable video generation output
Agnes Ai Generation by the numbers
- 2,005 all-time installs (skills.sh)
- +79 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #149 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yacey/agnes-ai-generation-skill --skill agnes-ai-generationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 376 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 21, 2026 |
| Repository | yacey/agnes-ai-generation-skill ↗ |
What it does
Generate text, images, and videos via Agnes AI APIs for content creation and media synthesis
Who is it for?
Teams building AI-driven content platforms, generative media workflows, multi-modal synthesis, and prompt-driven creative automation.
Skip if: Real-time synchronous video generation, multi-turn autonomous agent loops (function calling not reliable), or applications requiring offline inference.
When should I use this skill?
User requests text generation, image creation/editing, video synthesis, or Agnes model testing; when apihub.agnes-ai.com APIs must be called; when streaming output or async polling is needed.
What you get
Developers call agnes-ai-generation skill to create text, images, and videos at scale using fluent prompts, handle async video tasks, and inspect streaming output.
- Generated images
- Generated videos
- API integration scripts
Files
Agnes AI Generation
Use this skill to call Agnes text, image, and video generation APIs through https://apihub.agnes-ai.com.
Quick Start
1. Read references/api.md when endpoint details, parameters, or response fields are needed. 2. Use scripts/agnes_api.py for real API calls instead of rewriting curl by hand. 3. Require an API key in AGNES_API_KEY, AGNES_API_TOKEN, or APIHUB_AGNES_API_KEY. Never print the key. 4. For light live verification, run smoke-test; it avoids video creation by default. Add --include-image-edit for image-to-image, and add --video-case <case> explicitly for video modes. Treat the skill as fully tested only when basic text, text streaming, text tool calling, text-to-image, image-to-image, text-to-video, image-to-video, multi-image video, keyframe video, and video retrieval return successful responses.
Commands
Text generation:
python scripts/agnes_api.py text --prompt "Write a concise product tagline for an AI assistant."Streaming text:
python scripts/agnes_api.py text --prompt "Write a short product intro." --streamStreaming output is normalized and includes aggregated content, events, done, and a short raw_prefix.
Image generation:
python scripts/agnes_api.py image --prompt "A luminous floating city above a misty canyon at sunrise, cinematic realism" --size 1024x768Image-to-image:
python scripts/agnes_api.py image --prompt "Turn the scene into a rainy cyberpunk night while preserving composition" --image https://example.com/input.png --size 1024x768Text-to-video with polling:
python scripts/agnes_api.py video --prompt "A cinematic shot of a cat walking on the beach at sunset" --pollImage-to-video:
python scripts/agnes_api.py video --prompt "Animate subtle camera movement and natural lighting" --image https://example.com/image.png --pollKeyframe / multi-image video:
python scripts/agnes_api.py video --prompt "Create a smooth cinematic transition between the two keyframes" --image https://example.com/a.png --image https://example.com/b.png --mode keyframes --pollRetrieve a video task:
python scripts/agnes_api.py video-get video_123456Light live smoke test:
python scripts/agnes_api.py smoke-testImage edit smoke test:
python scripts/agnes_api.py smoke-test --include-image-editSingle video smoke test:
python scripts/agnes_api.py smoke-test --video-case text-to-videoWorkflow
- Prefer
agnes-2.0-flashfor text chat/completions. - Do not use Agnes Responses API multi-turn function calling for autonomous tool workflows. Live testing showed the provider can return
function_callwith overallstatus=completed, and submittingfunction_call_outputwithprevious_response_idmay fail. Use this skill's chat completions path for text generation and treat tool-calling as best-effort request-shape compatibility only. - Prefer
agnes-image-2.1-flashfor text-to-image, image-to-image, and high-information-density image generation. High-density generation is prompt-driven; include subject hierarchy, environment, secondary details, lighting, composition, and quality requirements. - Prefer
agnes-video-v2.0for text-to-video, image-to-video, multi-image video, keyframe animation, prompt-based motion and scene control, cinematic output, asynchronous task creation, polling-based result retrieval, and seed-based reproducibility. - For image and video generation, convert any non-English user prompt to a fluent English generation prompt before calling the image/video API. English prompts are more stable for Agnes video generation. Preserve concrete visual details, style, lighting, composition, motion, camera instructions, and constraints during translation.
- For videos, remember the API is asynchronous: create a task first, then poll or retrieve by
video_idwhen the create response includes it. The script falls back to legacytask_idlookup only whenvideo_idis absent. - The script validates image sizes, video frame counts, frame rates, and dimensions before sending requests.
num_framesmust be8n + 1and<= 441;81or121are good short values. - The video command defaults to
num_frames=121andframe_rate=24for more stable generation. Video smoke tests default tonum_frames=81andframe_rate=24. - Warn the user before costly or long-running live video generation unless they explicitly asked to test or generate video.
- Test video capabilities one at a time with
smoke-test --video-case <case>to avoid creating many tasks at once. Supported cases aretext-to-video,image-to-video,multi-image, andkeyframes.
Current Validation Notes
- Confirmed locally: skill metadata validation and Python syntax.
- Confirmed by live API: basic text, streaming text, tool-calling request shape, text-to-image, image-to-image, high-information-density text-to-image, Chinese prompt translation for image/video, completed text-to-video URL retrieval, and completed image-to-video URL retrieval.
- Caveat: Agnes may accept tool-calling request parameters without consistently returning
tool_calls; usesmoke-test --strict-toolswhen strict tool-call validation is required. - Caveat: Agnes Responses API multi-turn function calling is not reliable for agent tool loops; do not rely on it for Codex/Claude-style automatic tool continuation.
- Supported by the script and smoke-test selector, but not re-run end-to-end in the latest pass: multi-image video and keyframe animation.
- Not yet confirmed end-to-end: completed URL retrieval for every multi-image video and keyframe animation task. A previous text-to-video task returned a provider-side
division by zeroerror, so keep video retries visible and report provider errors clearly.
Output Handling
- Return generated image/video URLs directly by default. Do not download, save, open, or inspect generated media unless the user explicitly asks for a local file or visual inspection.
- For image responses, expect URL-style results when
extra_body.response_formatisurl. - For video responses, extract URLs from
video_url,url, orremixed_from_video_idwhenstatusiscompleted. - For video retrieval, prefer
GET /agnesapi?video_id=...&model_name=agnes-video-v2.0; legacyGET /v1/videos/{task_id}remains a fallback. - If a request fails, report HTTP status and provider error body without exposing the API key.
__pycache__/
*.py[cod]
.env
.env.*
.aider-desk/
interface:
display_name: "Agnes AI Generation"
short_description: "Call Agnes text, image, and video generation APIs."
default_prompt: "Use Agnes AI to generate text, images, or videos via the official API."
MIT License
Copyright (c) 2026 Yacey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Agnes AI Generation Skill
中文
Agnes AI Generation Skill is a standard Agent Skills package for calling Agnes AI text, image, and video generation APIs. It can be installed into Codex, Claude Code, OpenClaw, Cursor, Windsurf, and other clients that support Agent Skills. After installation, AI agents can use Agnes models for text generation, text-to-image, image-to-image, text-to-video, image-to-video, multi-image video, and keyframe animation workflows.
Official platform: https://platform.agnes-ai.com/
Latest release: v0.1.0 - Agnes Generation Skill
This release prioritizes the newer Agnes Video V2.0 video_id result lookup, returns generated media urls by default without downloading files, and documents that Agnes Responses API multi-turn function calling is not suitable for agent automatic tool loops yet. See Releases for the full notes.
Features
- Text generation with
agnes-2.0-flash - Streaming text responses
- OpenAI-compatible tool-calling request shape
- Text-to-image with
agnes-image-2.1-flash - Image-to-image editing with
agnes-image-2.1-flash - High-information-density image generation
- Text-to-video with
agnes-video-v2.0 - Image-to-video with
agnes-video-v2.0 - Multi-image video generation
- Keyframe animation
- Prompt-based motion and scene control
- Cinematic video output
- Asynchronous video task creation
- Polling-based video result retrieval
- Seed-based reproducibility
- Automatic English prompt translation for non-English image/video prompts
Quick Start
1. Apply for an Agnes API Key
1. Open https://platform.agnes-ai.com/. 2. Register or sign in. 3. Create an API key from the platform. 4. Provide the API key to the AI in a trusted current session, or configure it as a local environment variable.
Do not commit API keys to Git, README files, screenshots, or public chat logs.
2. Install This Skill
Install into the current agent:
npx skills add Yacey/agnes-ai-generation-skillInstall into all supported agents:
npx skills add Yacey/agnes-ai-generation-skill --all3. Configure the API Key
For the current PowerShell session:
$env:AGNES_API_KEY="YOUR_API_KEY"For persistent Windows user-level configuration:
[Environment]::SetEnvironmentVariable("AGNES_API_KEY", "YOUR_API_KEY", "User")The script also accepts:
AGNES_API_KEYAGNES_API_TOKENAPIHUB_AGNES_API_KEY
4. Use the Skill
Ask your AI agent:
Use Agnes to generate a high-information-density futuristic city image.Or:
Use Agnes to turn this image into a cinematic video.Usage Examples
Text:
python scripts/agnes_api.py text --prompt "Write a concise product tagline for an AI assistant."Text-to-image:
python scripts/agnes_api.py image --prompt "A luminous floating city above a misty canyon at sunrise, cinematic realism" --size 1024x768Image-to-image:
python scripts/agnes_api.py image --prompt "Turn the scene into a rainy cyberpunk night while preserving composition" --image https://example.com/input.pngText-to-video:
python scripts/agnes_api.py video --prompt "A cinematic shot of a cat walking on the beach at sunset" --pollVideo commands default to --num-frames 121 --frame-rate 24 to reduce instability from missing core video parameters. The script validates num_frames before sending requests: it must satisfy 8n + 1 and be no more than 441. It also checks frame rate and dimensions.
Image-to-video:
python scripts/agnes_api.py video --prompt "Animate subtle camera movement and natural lighting" --image https://example.com/image.png --pollMulti-image / keyframe video:
python scripts/agnes_api.py video --prompt "Create a smooth cinematic transition between the two keyframes" --image https://example.com/a.png --image https://example.com/b.png --mode keyframes --pollRetrieve a video task:
python scripts/agnes_api.py video-get video_123456By default, command output is normalized with common fields such as content, urls, translated_prompt, and next_steps, while preserving the provider response under raw.
By default, return generated image or video urls directly. Do not download, save, or inspect media locally unless the user explicitly asks for a local file or visual inspection.
Streaming text output also includes aggregated content, event count, completion status, and a short raw prefix so the result is easier to inspect.
When the create response includes video_id, the script prefers the newer result endpoint: /agnesapi?video_id=.... It falls back to the legacy task_id endpoint only when video_id is absent. For completed video tasks, the script extracts direct mp4 URLs from video_url, url, or the live-response field remixed_from_video_id, then places them in urls.
Note: Agnes Responses API multi-turn function calling is currently not suitable as the automatic tool-loop model for agents such as Codex or Claude Code. This skill's script uses the chat completions path; tool calling should be treated as request-shape compatibility rather than stable multi-turn tool execution.
Add --raw to print only the original Agnes response:
--rawLight smoke test. By default, this checks text, streaming text, tool-calling request shape, and text-to-image only. It does not create video tasks:
python scripts/agnes_api.py smoke-testThe tool-calling request can be accepted by Agnes without returning tool_calls consistently. By default this is reported as a warning. Use strict mode to fail in that case:
python scripts/agnes_api.py smoke-test --strict-toolsImage-to-image smoke test:
python scripts/agnes_api.py smoke-test --include-image-editSingle video case test:
python scripts/agnes_api.py smoke-test --video-case text-to-videoPrompt Language
English prompts are more stable for Agnes video generation. For image and video calls, this skill automatically translates non-English prompts to English before sending them to the Agnes image/video APIs. It preserves subjects, scene details, style, lighting, composition, camera movement, motion, and constraints.
To disable automatic translation:
python scripts/agnes_api.py video --prompt "non-English prompt" --no-translate-promptValidation Status
Confirmed by live API:
- Basic text
- Streaming text
- Tool-calling request shape
- Text-to-image
- Image-to-image
- High-information-density text-to-image
- Non-English prompt auto-translation for text-to-image
- Non-English prompt auto-translation for text-to-video task creation
- Completed text-to-video retrieval with a direct mp4 URL
- Completed image-to-video retrieval with a direct mp4 URL
Supported but not fully re-tested end-to-end in the latest pass:
- Multi-image video
- Keyframe video
Not fully confirmed end-to-end yet:
- Completed URL retrieval for every multi-image video and keyframe animation task
A previous live text-to-video retrieval returned a provider-side division by zero error; a later short text-to-video task completed successfully and returned an mp4 URL. Keep provider errors visible when retrying video modes.
License
MIT License. See LICENSE.
Agnes AI Generation Skill
English
用于 Agent Skills 的 Agnes AI 生成技能,封装 Agnes 官方文本、图片、视频 API。它采用标准 SKILL.md 结构,可安装到 Codex、Claude Code、OpenClaw、Cursor、Windsurf 等支持 Agent Skills 的客户端。安装后,你可以让 AI 直接调用 Agnes 模型完成文生图、图生图、文生视频、图生视频、多图视频、关键帧动画等操作。
官网与 API 平台:https://platform.agnes-ai.com/
最新版本:v0.1.0 - Agnes Generation Skill
本版本重点修复:Agnes Video V2.0 优先使用新版 video_id 查询接口;生成图片/视频后默认只返回 urls,不主动下载媒体;明确 Agnes Responses API 多轮函数调用暂不适合作为 agent 自动工具循环。完整说明见 Releases。
功能
- 文本生成:
agnes-2.0-flash - 流式文本响应
- OpenAI 兼容的工具调用请求结构
- 文生图:
agnes-image-2.1-flash - 图生图 / 图片编辑:
agnes-image-2.1-flash - 高信息密度图片生成
- 文本转视频:
agnes-video-v2.0 - 图像转视频:
agnes-video-v2.0 - 多图像视频生成
- 关键帧动画
- 基于提示词的运动与场景控制
- 电影感视觉输出
- 异步视频任务创建
- 轮询式视频结果检索
- 基于 seed 的可重复生成
- 自动将非英文图片/视频提示词翻译为英文提示词,提高 Agnes 视频生成稳定性
快速开始
1. 申请 Agnes API Key
1. 打开 Agnes API 平台:https://platform.agnes-ai.com/ 2. 注册或登录账号。 3. 在平台中申请 / 创建 API Key。 4. 拿到 API Key 后,可以在可信任的当前 AI 会话里发送给 AI,或配置为本机环境变量。
安全提醒:不要把 API Key 写入 Git 仓库、README、截图或公开聊天记录。
2. 安装本 Skill
安装到当前 Agent:
npx skills add Yacey/agnes-ai-generation-skill安装到所有支持的 Agent:
npx skills add Yacey/agnes-ai-generation-skill --all安装后,当你要求已安装的 AI Agent 使用 Agnes 生成图片或视频时,AI 会自动触发本 skill。
3. 配置 API Key
临时配置当前 PowerShell 会话:
$env:AGNES_API_KEY="YOUR_API_KEY"Windows 用户级持久配置:
[Environment]::SetEnvironmentVariable("AGNES_API_KEY", "YOUR_API_KEY", "User")脚本也会识别以下变量名:
AGNES_API_KEYAGNES_API_TOKENAPIHUB_AGNES_API_KEY
4. 开始使用
你可以直接对 AI 说:
使用 Agnes 帮我生成一张高信息密度的未来城市图片。或:
使用 Agnes 把这张图片生成一段电影感视频。如果你已经把 API Key 发送给当前 AI,AI 可以配置环境变量并调用本 skill。之后即可解锁生图、生视频等相关操作。
命令示例
文本生成:
python scripts/agnes_api.py text --prompt "Write a concise product tagline for an AI assistant."文生图:
python scripts/agnes_api.py image --prompt "A luminous floating city above a misty canyon at sunrise, cinematic realism" --size 1024x768中文提示词文生图,脚本会先自动翻译成英文:
python scripts/agnes_api.py image --prompt "一座高信息密度的未来城市集市,拥挤人群,飞行汽车,全息招牌,电影感写实风格"图生图:
python scripts/agnes_api.py image --prompt "Turn the scene into a rainy cyberpunk night while preserving composition" --image https://example.com/input.png文生视频:
python scripts/agnes_api.py video --prompt "A cinematic shot of a cat walking on the beach at sunset" --poll视频命令默认使用 --num-frames 121 --frame-rate 24,以减少缺少关键视频参数导致的不稳定。脚本会在请求前检查 num_frames 是否满足 8n + 1 且不超过 441,并检查帧率、尺寸等基础参数。
图生视频:
python scripts/agnes_api.py video --prompt "Animate subtle camera movement and natural lighting" --image https://example.com/image.png --poll多图 / 关键帧视频:
python scripts/agnes_api.py video --prompt "Create a smooth cinematic transition between the two keyframes" --image https://example.com/a.png --image https://example.com/b.png --mode keyframes --poll查询视频任务:
python scripts/agnes_api.py video-get video_123456默认输出会整理出常用字段,例如 content、urls、translated_prompt、next_steps,同时保留 raw 原始响应,方便 AI 和人工继续处理。
默认情况下,生成图片或视频后直接返回 urls 即可;除非用户明确要求下载、保存或检查本地文件,否则不需要把媒体下载到本地。
流式文本也会聚合输出 content,同时保留事件数量、是否完成和原始响应前缀,便于快速判断流式接口是否正常。
视频创建接口如果返回 video_id,脚本会优先使用新版 video_id 查询接口:/agnesapi?video_id=...。如果旧响应没有 video_id,脚本才回退到兼容的 task_id 查询接口。视频完成后,脚本会从 video_url、url 或 Agnes 实测返回中的 remixed_from_video_id 提取可直接访问的 mp4 链接,并放入 urls。
注意:Agnes Responses API 的多轮函数调用目前不适合作为 Codex / Claude Code 这类 agent 的自动工具循环模型。本 skill 的脚本使用 chat completions 路径;工具调用只应视为请求结构兼容能力,而不是稳定的多轮工具执行能力。
若只想看 Agnes 原始响应,可以加:
--raw运行测试:
python scripts/agnes_api.py smoke-test默认测试覆盖文本、流式文本、工具调用请求结构和文生图,不会创建视频任务。工具调用请求有时会被 Agnes 接收但不返回 tool_calls,默认只输出 warning;如果你想把这种情况视为失败,可以使用:
python scripts/agnes_api.py smoke-test --strict-tools测试图生图:
python scripts/agnes_api.py smoke-test --include-image-edit单独测试某个视频能力,避免一次创建太多视频任务:
python scripts/agnes_api.py smoke-test --video-case text-to-video可选的视频测试项:
text-to-videoimage-to-videomulti-imagekeyframes
提示词语言策略
Agnes 视频生成使用英文提示词更稳定。因此本 skill 的脚本默认会检测图片/视频提示词中的非英文字符,并先调用 agnes-2.0-flash 翻译成英文生成提示词,再调用图片或视频 API。
翻译时会保留:
- 主体
- 场景
- 风格
- 光照
- 构图
- 镜头运动
- 动作描述
- 负面提示词或约束
如果你确实想跳过自动翻译,可以加:
--no-translate-prompt示例:
python scripts/agnes_api.py video --prompt "中文提示词" --no-translate-prompt测试状态
已通过真实 API 测试:
- 基础文本生成
- 流式文本
- 工具调用请求结构
- 文生图
- 图生图
- 高信息密度文生图
- 中文提示词自动翻译后文生图
- 中文提示词自动翻译后文生视频任务创建
- 文生视频完成并返回 mp4 URL
- 图生视频完成并返回 mp4 URL
已支持但本轮未完整端到端重测:
- 多图视频
- 关键帧视频
尚未完整端到端验证:
- 每一种多图视频 / 关键帧视频任务都成功返回最终视频 URL
说明:曾有一次真实文生视频任务在查询时返回 Agnes 服务端 division by zero 错误;后续短文生视频任务已成功完成并返回 mp4 URL。因此视频能力在 skill 中已支持,但仍建议逐个模式测试并保留 provider 错误信息。
仓库结构
.
├── SKILL.md
├── README.md
├── README_EN.md
├── LICENSE
├── agents/
│ └── openai.yaml
├── references/
│ └── api.md
└── scripts/
└── agnes_api.py许可证
MIT License. See LICENSE.
Agnes AI API Reference
Base host: https://apihub.agnes-ai.com
Authentication: Authorization: Bearer YOUR_API_KEY
Content type: application/json
Text
Endpoint: POST /v1/chat/completions
Model: agnes-2.0-flash
Required:
model: fixed asagnes-2.0-flashmessages: OpenAI-compatible chat messages
Optional:
temperature: numbertop_p: numbermax_tokens: numberstream: booleantools: arraytool_choice: string or object
Response is OpenAI-compatible and includes choices[].message.content and usage.
Image
Endpoint: POST /v1/images/generations
Model: agnes-image-2.1-flash
Required:
model: fixed asagnes-image-2.1-flashprompt: text instruction for image generation or editing
Optional:
size: output size such as1024x768extra_body.image: array of input image URLs for image-to-imageextra_body.response_format: useurlfor image URLs
Prompt structure:
[Subject] + [Scene / Environment] + [Style] + [Lighting] + [Composition] + [Quality Requirements]
For image-to-image, state what should change and what must remain unchanged.
For non-English user prompts, translate to English before sending the request. Preserve visual specifics and constraints.
Video
Create task endpoint: POST /v1/videos
Recommended result endpoint: GET /agnesapi?video_id={video_id}
Legacy task endpoint: GET /v1/videos/{task_id}
Model: agnes-video-v2.0
The video API is asynchronous. Create a task, then retrieve or poll by the returned video_id when present. Fall back to task_id only for older responses.
Use English prompts for video generation whenever possible. If the user prompt is not English, translate it to English first, preserving subject, action, scene, camera movement, lighting, style, and constraints.
Required:
model: fixed asagnes-video-v2.0prompt: text description of the video
Optional:
image: input image URL or image URL array for image-to-videomode: generation mode such asti2vidorkeyframesheight: integer, default768width: integer, default1152num_frames: integer, must be<= 441and satisfy8n + 1num_inference_steps: integerseed: integerframe_rate: number, supported range1-60negative_prompt: stringextra_body.image: array for multi-image video or keyframe modeextra_body.mode: set tokeyframesfor keyframe animation
Common status values:
queuedin_progresscompletedfailed
The create response may include both task_id and video_id; video_id is the recommended lookup identifier for new integrations. The completed response usually includes a video URL. In live responses this may appear as video_url, url, or remixed_from_video_id, plus size, seconds, and usage.duration_seconds.
Recommended video defaults:
- Standard:
width=1152,height=768,num_frames=121,frame_rate=24 - Short smoke test:
num_frames=81,frame_rate=24 - Reproducibility: set
seed
Error Codes
400: invalid request401: unauthorized; check API key404: task not found500: server error503: service busy; retry later
#!/usr/bin/env python3
"""Small CLI for Agnes AI text, image, and video generation APIs."""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
import urllib.parse
import urllib.error
import urllib.request
from typing import Any
BASE_URL = "https://apihub.agnes-ai.com"
TEXT_MODEL = "agnes-2.0-flash"
IMAGE_MODEL = "agnes-image-2.1-flash"
VIDEO_MODEL = "agnes-video-v2.0"
SIZE_RE = re.compile(r"^[1-9]\d*x[1-9]\d*$")
def get_api_key() -> str:
for name in ("AGNES_API_KEY", "AGNES_API_TOKEN", "APIHUB_AGNES_API_KEY"):
value = os.environ.get(name)
if value:
return value
raise SystemExit(
"Missing API key. Set AGNES_API_KEY, AGNES_API_TOKEN, or APIHUB_AGNES_API_KEY."
)
def request_json(method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
body = None if payload is None else json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
BASE_URL + path,
data=body,
method=method,
headers={
"Authorization": f"Bearer {get_api_key()}",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
text = resp.read().decode("utf-8")
return json.loads(text) if text else {}
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise SystemExit(f"HTTP {exc.code} from {path}: {detail}") from exc
except urllib.error.URLError as exc:
raise SystemExit(f"Request failed for {path}: {exc}") from exc
def request_text(method: str, path: str, payload: dict[str, Any] | None = None) -> str:
body = None if payload is None else json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
BASE_URL + path,
data=body,
method=method,
headers={
"Authorization": f"Bearer {get_api_key()}",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise SystemExit(f"HTTP {exc.code} from {path}: {detail}") from exc
except urllib.error.URLError as exc:
raise SystemExit(f"Request failed for {path}: {exc}") from exc
def stream_summary(payload: dict[str, Any]) -> dict[str, Any]:
raw = request_text("POST", "/v1/chat/completions", payload)
event_count = 0
done = False
content_parts: list[str] = []
for line in raw.splitlines():
line = line.strip()
if not line.startswith("data:"):
continue
data = line.removeprefix("data:").strip()
if data == "[DONE]":
done = True
elif data:
event_count += 1
try:
event = json.loads(data)
except json.JSONDecodeError:
continue
try:
delta = event["choices"][0].get("delta", {})
except (KeyError, IndexError, TypeError, AttributeError):
continue
content = delta.get("content")
if isinstance(content, str):
content_parts.append(content)
return {
"type": "text-stream",
"content": "".join(content_parts) or None,
"events": event_count,
"done": done,
"raw_prefix": raw[:200],
}
def print_json(data: Any) -> None:
print(json.dumps(data, ensure_ascii=False, indent=2))
def parse_json_arg(name: str, value: str) -> Any:
try:
return json.loads(value)
except json.JSONDecodeError as exc:
raise SystemExit(f"Invalid JSON for {name}: {exc.msg} at position {exc.pos}") from exc
def needs_english_translation(prompt: str) -> bool:
return any(ord(ch) > 127 for ch in prompt)
def translate_prompt_to_english(prompt: str) -> str:
payload = {
"model": TEXT_MODEL,
"messages": [
{
"role": "system",
"content": (
"Translate the user's image/video generation prompt into fluent English. "
"Preserve all concrete visual details, style words, camera motion, lighting, "
"composition constraints, and negative instructions. Return only the English prompt."
),
},
{"role": "user", "content": prompt},
],
"temperature": 0,
"max_tokens": 800,
}
data = request_json("POST", "/v1/chat/completions", payload)
try:
translated = data["choices"][0]["message"]["content"].strip()
except (KeyError, IndexError, TypeError) as exc:
raise SystemExit(f"Prompt translation failed: {json.dumps(data, ensure_ascii=False)}") from exc
if not translated:
raise SystemExit("Prompt translation failed: empty translated prompt")
return translated
def prepare_generation_prompt(prompt: str, translate: bool = True) -> tuple[str, str | None]:
if translate and needs_english_translation(prompt):
translated = translate_prompt_to_english(prompt)
return translated, translated
return prompt, None
def extract_text_content(data: dict[str, Any]) -> str | None:
try:
content = data["choices"][0]["message"].get("content")
except (KeyError, IndexError, TypeError, AttributeError):
return None
return content if isinstance(content, str) else None
def output_result(
result_type: str,
raw: dict[str, Any],
*,
prompt_used: str | None = None,
translated_prompt: str | None = None,
urls: list[str] | None = None,
status: str | None = None,
next_steps: list[str] | None = None,
raw_only: bool = False,
) -> None:
if raw_only:
print_json(raw)
return
summary: dict[str, Any] = {"type": result_type}
if status:
summary["status"] = status
if urls:
summary["urls"] = urls
if prompt_used:
summary["prompt_used"] = prompt_used
if translated_prompt:
summary["translated_prompt"] = translated_prompt
if next_steps:
summary["next_steps"] = next_steps
summary["raw"] = raw
print_json(summary)
def extract_image_urls(data: dict[str, Any]) -> list[str]:
urls = []
if isinstance(data.get("url"), str):
urls.append(data["url"])
if isinstance(data.get("image_url"), str):
urls.append(data["image_url"])
if isinstance(data.get("data"), list):
for item in data["data"]:
if isinstance(item, dict):
for key in ("url", "image_url"):
if isinstance(item.get(key), str):
urls.append(item[key])
return urls
def extract_video_urls(data: dict[str, Any]) -> list[str]:
urls = []
for key in ("video_url", "url", "remixed_from_video_id"):
value = data.get(key)
if isinstance(value, str) and value.startswith(("http://", "https://")):
urls.append(value)
if isinstance(data.get("data"), list):
for item in data["data"]:
if isinstance(item, dict):
urls.extend(extract_video_urls(item))
return list(dict.fromkeys(urls))
def video_lookup_id(data: dict[str, Any]) -> tuple[str | None, str | None]:
video_id = data.get("video_id")
if isinstance(video_id, str) and video_id:
return video_id, "video_id"
for key in ("task_id", "id"):
value = data.get(key)
if isinstance(value, str) and value:
return value, "task_id"
return None, None
def retrieve_video(identifier: str, model_name: str | None = VIDEO_MODEL) -> dict[str, Any]:
if identifier.startswith("video_"):
query = {"video_id": identifier}
if model_name:
query["model_name"] = model_name
return request_json("GET", "/agnesapi?" + urllib.parse.urlencode(query))
return request_json("GET", f"/v1/videos/{urllib.parse.quote(identifier, safe='')}")
def validate_size(value: str | None, name: str = "size") -> None:
if value and not SIZE_RE.match(value):
raise SystemExit(f"Invalid {name}: {value}. Expected WIDTHxHEIGHT, for example 1024x768.")
def validate_video_args(args: argparse.Namespace) -> None:
if args.num_frames is not None:
if args.num_frames > 441 or (args.num_frames - 1) % 8 != 0:
raise SystemExit("Invalid --num-frames: must be <= 441 and satisfy 8n + 1, for example 81 or 121.")
if args.frame_rate is not None and not (1 <= args.frame_rate <= 60):
raise SystemExit("Invalid --frame-rate: supported range is 1-60.")
for name in ("height", "width"):
value = getattr(args, name)
if value is not None and value <= 0:
raise SystemExit(f"Invalid --{name.replace('_', '-')}: must be a positive integer.")
def cmd_text(args: argparse.Namespace) -> None:
messages = []
if args.system:
messages.append({"role": "system", "content": args.system})
messages.append({"role": "user", "content": args.prompt})
payload: dict[str, Any] = {
"model": TEXT_MODEL,
"messages": messages,
"temperature": args.temperature,
"max_tokens": args.max_tokens,
}
if args.top_p is not None:
payload["top_p"] = args.top_p
if args.stream:
payload["stream"] = True
if args.tools_json:
payload["tools"] = parse_json_arg("--tools-json", args.tools_json)
if args.tool_choice_json:
payload["tool_choice"] = parse_json_arg("--tool-choice-json", args.tool_choice_json)
if args.stream:
print_json(stream_summary(payload))
else:
data = request_json("POST", "/v1/chat/completions", payload)
content = extract_text_content(data)
wrapped = {
"type": "text",
"content": content,
"raw": data,
}
print_json(data if args.raw else wrapped)
def cmd_image(args: argparse.Namespace) -> None:
validate_size(args.size)
prompt, translated_prompt = prepare_generation_prompt(args.prompt, not args.no_translate_prompt)
payload: dict[str, Any] = {
"model": IMAGE_MODEL,
"prompt": prompt,
}
if args.size:
payload["size"] = args.size
extra: dict[str, Any] = {"response_format": "url"}
if args.image:
extra["image"] = args.image
if extra:
payload["extra_body"] = extra
data = request_json("POST", "/v1/images/generations", payload)
urls = extract_image_urls(data)
output_result(
"image-to-image" if args.image else "text-to-image",
data,
prompt_used=prompt,
translated_prompt=translated_prompt,
urls=urls,
raw_only=args.raw,
)
def video_payload(args: argparse.Namespace) -> dict[str, Any]:
validate_video_args(args)
prompt, translated_prompt = prepare_generation_prompt(args.prompt, not args.no_translate_prompt)
args._prompt_used = prompt
args._translated_prompt = translated_prompt
payload: dict[str, Any] = {
"model": VIDEO_MODEL,
"prompt": prompt,
}
for name in (
"height",
"width",
"num_frames",
"frame_rate",
"num_inference_steps",
"seed",
"negative_prompt",
):
value = getattr(args, name)
if value is not None:
payload[name] = value
if args.mode:
payload["mode"] = args.mode
if args.image:
if len(args.image) == 1 and args.mode != "keyframes":
payload["image"] = args.image[0]
else:
payload["extra_body"] = {"image": args.image}
if args.mode:
payload["extra_body"]["mode"] = args.mode
return payload
def poll_video(identifier: str, timeout: int, interval: int) -> dict[str, Any]:
deadline = time.time() + timeout
last: dict[str, Any] = {}
while time.time() < deadline:
last = retrieve_video(identifier)
if last.get("error"):
raise SystemExit(f"Video {identifier} returned error: {json.dumps(last, ensure_ascii=False)}")
status = str(last.get("status", "")).lower()
progress = last.get("progress")
if status:
print(f"video {identifier}: status={status} progress={progress}", file=sys.stderr)
if status in {"completed", "failed"}:
return last
time.sleep(interval)
raise SystemExit(f"Timed out waiting for video {identifier}. Last response: {json.dumps(last)}")
def cmd_video(args: argparse.Namespace) -> None:
created = request_json("POST", "/v1/videos", video_payload(args))
if not args.poll:
identifier, id_kind = video_lookup_id(created)
next_steps = []
if identifier:
next_steps.append(f"python scripts/agnes_api.py video-get {identifier}")
next_steps.append(f"python scripts/agnes_api.py video-get {identifier} # repeat until status is completed")
output_result(
"video-task",
created,
prompt_used=getattr(args, "_prompt_used", None),
translated_prompt=getattr(args, "_translated_prompt", None),
status=str(created.get("status", "")) if created.get("status") is not None else None,
next_steps=next_steps,
raw_only=args.raw,
)
if id_kind == "task_id":
print("warning: create response did not include video_id; falling back to legacy task_id lookup", file=sys.stderr)
return
identifier, id_kind = video_lookup_id(created)
if not identifier:
raise SystemExit(f"Video create response did not include video_id, task_id, or id: {json.dumps(created)}")
if id_kind == "task_id":
print("warning: create response did not include video_id; falling back to legacy task_id lookup", file=sys.stderr)
data = poll_video(identifier, args.timeout, args.interval)
urls = extract_video_urls(data)
output_result(
"video-result",
data,
prompt_used=getattr(args, "_prompt_used", None),
translated_prompt=getattr(args, "_translated_prompt", None),
urls=urls,
status=str(data.get("status", "")) if data.get("status") is not None else None,
raw_only=args.raw,
)
def cmd_video_get(args: argparse.Namespace) -> None:
data = retrieve_video(args.identifier, None if args.no_model_name else args.model_name)
urls = extract_video_urls(data)
output_result(
"video-result",
data,
urls=urls,
status=str(data.get("status", "")) if data.get("status") is not None else None,
next_steps=[] if urls else [f"python scripts/agnes_api.py video-get {args.identifier}"],
raw_only=args.raw,
)
if data.get("error"):
raise SystemExit(1)
def require_ok(name: str, data: dict[str, Any], keys: tuple[str, ...]) -> None:
missing = [key for key in keys if key not in data]
if missing:
raise SystemExit(f"{name} response missing {missing}: {json.dumps(data)}")
print(f"{name}: ok")
def require_video_ok(name: str, data: dict[str, Any], completed: bool = False) -> None:
require_ok(name, data, ("id", "status"))
if data.get("error"):
raise SystemExit(f"{name} returned error: {json.dumps(data, ensure_ascii=False)}")
status = str(data.get("status", "")).lower()
if status == "failed":
raise SystemExit(f"{name} failed: {json.dumps(data, ensure_ascii=False)}")
if completed and status != "completed":
raise SystemExit(f"{name} did not complete: {json.dumps(data, ensure_ascii=False)}")
if completed and not extract_video_urls(data):
raise SystemExit(f"{name} completed without a video URL: {json.dumps(data, ensure_ascii=False)}")
def check_tool_call(name: str, data: dict[str, Any], strict: bool = False) -> None:
try:
tool_calls = data["choices"][0]["message"].get("tool_calls")
except (KeyError, IndexError, TypeError, AttributeError):
tool_calls = None
if not tool_calls:
message = f"{name}: request accepted, but response did not include tool_calls"
if strict:
raise SystemExit(f"{message}: {json.dumps(data, ensure_ascii=False)}")
print(message, file=sys.stderr)
return
print(f"{name}: ok")
def extract_image_url(data: dict[str, Any]) -> str:
candidates = extract_image_urls(data)
if not candidates:
raise SystemExit(f"Could not find image URL in response: {json.dumps(data, ensure_ascii=False)}")
return candidates[0]
def create_video_case(name: str, payload: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:
created = request_json("POST", "/v1/videos", payload)
require_video_ok(f"{name}-create", created)
identifier, id_kind = video_lookup_id(created)
if not identifier:
raise SystemExit(f"{name}-create response missing video_id/task_id/id: {json.dumps(created, ensure_ascii=False)}")
if id_kind == "task_id":
print(f"{name}-create: warning: falling back to legacy task_id lookup", file=sys.stderr)
retrieved = (
poll_video(identifier, args.video_timeout, args.video_interval)
if args.poll_video
else retrieve_video(identifier)
)
require_video_ok(f"{name}-get", retrieved, completed=args.poll_video)
return {"create": created, "get": retrieved}
VIDEO_CASES = ("text-to-video", "image-to-video", "multi-image", "keyframes")
def cmd_smoke_test(args: argparse.Namespace) -> None:
validate_size(args.image_size, "image-size")
validate_video_args(
argparse.Namespace(
num_frames=args.video_num_frames,
frame_rate=args.video_frame_rate,
height=args.video_height,
width=args.video_width,
)
)
text = request_json(
"POST",
"/v1/chat/completions",
{
"model": TEXT_MODEL,
"messages": [{"role": "user", "content": "Reply with exactly: Agnes text ok"}],
"max_tokens": 20,
"temperature": 0,
},
)
require_ok("text", text, ("choices",))
text_stream = stream_summary(
{
"model": TEXT_MODEL,
"messages": [{"role": "user", "content": "Reply with exactly: Agnes stream ok"}],
"max_tokens": 20,
"temperature": 0,
"stream": True,
}
)
if text_stream["events"] < 1 and not text_stream["done"]:
raise SystemExit(f"text-stream response did not look like SSE: {json.dumps(text_stream)}")
print("text-stream: ok")
text_tools = request_json(
"POST",
"/v1/chat/completions",
{
"model": TEXT_MODEL,
"messages": [{"role": "user", "content": "Use the get_test_value tool."}],
"max_tokens": 128,
"temperature": 0,
"tools": [
{
"type": "function",
"function": {
"name": "get_test_value",
"description": "Return a deterministic smoke test value.",
"parameters": {
"type": "object",
"properties": {
"label": {"type": "string", "description": "test label"}
},
"required": ["label"],
},
},
}
],
"tool_choice": {"type": "function", "function": {"name": "get_test_value"}},
},
)
check_tool_call("text-tools", text_tools, strict=args.strict_tools)
image_text = request_json(
"POST",
"/v1/images/generations",
{
"model": IMAGE_MODEL,
"prompt": "A simple red square icon centered on a white background",
"size": args.image_size,
"extra_body": {"response_format": "url"},
},
)
require_ok("image-text-to-image", image_text, ("data",))
generated_image_url = extract_image_url(image_text)
image_edit = None
edited_image_url = None
selected_cases = set(args.video_case or [])
needs_second_image = bool(selected_cases.intersection({"multi-image", "keyframes"}))
if args.include_image_edit or needs_second_image:
image_edit = request_json(
"POST",
"/v1/images/generations",
{
"model": IMAGE_MODEL,
"prompt": "Turn this into a clean blue square icon while preserving the centered composition",
"size": args.image_size,
"extra_body": {"image": [generated_image_url], "response_format": "url"},
},
)
require_ok("image-to-image", image_edit, ("data",))
edited_image_url = extract_image_url(image_edit)
video_common = {
"model": VIDEO_MODEL,
}
for key, value in (
("height", args.video_height),
("width", args.video_width),
("num_frames", args.video_num_frames),
("frame_rate", args.video_frame_rate),
):
if value is not None:
video_common[key] = value
video_results = {}
if "text-to-video" in selected_cases:
video_results["text_to_video"] = create_video_case(
"video-text-to-video",
{
**video_common,
"prompt": "A simple cinematic shot of a red square gently moving on a white background",
},
args,
)
if "image-to-video" in selected_cases:
video_results["image_to_video"] = create_video_case(
"video-image-to-video",
{
**video_common,
"prompt": "Animate the icon with subtle floating motion, stable centered composition",
"image": generated_image_url,
},
args,
)
if "multi-image" in selected_cases:
if not edited_image_url:
raise SystemExit("multi-image test requires an edited image URL")
video_results["multi_image"] = create_video_case(
"video-multi-image",
{
**video_common,
"prompt": "Create a smooth transformation from the first icon to the second icon, stable centered composition",
"extra_body": {"image": [generated_image_url, edited_image_url]},
},
args,
)
if "keyframes" in selected_cases:
if not edited_image_url:
raise SystemExit("keyframes test requires an edited image URL")
video_results["keyframes"] = create_video_case(
"video-keyframes",
{
**video_common,
"prompt": "Create a smooth keyframe transition between the two icons, stable centered composition",
"extra_body": {"image": [generated_image_url, edited_image_url], "mode": "keyframes"},
},
args,
)
print_json(
{
"text": text,
"text_stream": text_stream,
"text_tools": text_tools,
"image_text_to_image": image_text,
"image_to_image": image_edit,
"video": video_results,
}
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Call Agnes AI generation APIs.")
sub = parser.add_subparsers(dest="command", required=True)
text = sub.add_parser("text", help="Create a chat completion.")
text.add_argument("--prompt", required=True)
text.add_argument("--system")
text.add_argument("--temperature", type=float, default=0.7)
text.add_argument("--top-p", type=float)
text.add_argument("--max-tokens", type=int, default=1024)
text.add_argument("--stream", action="store_true")
text.add_argument("--tools-json", help="JSON array for OpenAI-compatible tool definitions.")
text.add_argument("--tool-choice-json", help="JSON object/string for OpenAI-compatible tool_choice.")
text.add_argument("--raw", action="store_true", help="Print the raw provider response.")
text.set_defaults(func=cmd_text)
image = sub.add_parser("image", help="Generate or edit an image.")
image.add_argument("--prompt", required=True)
image.add_argument("--size", default="1024x768")
image.add_argument("--image", action="append", help="Input image URL. Repeat for multiple images.")
image.add_argument(
"--no-translate-prompt",
action="store_true",
help="Do not translate non-English prompts before sending to the image API.",
)
image.add_argument("--raw", action="store_true", help="Print the raw provider response.")
image.set_defaults(func=cmd_image)
video = sub.add_parser("video", help="Create a video task.")
video.add_argument("--prompt", required=True)
video.add_argument("--image", action="append", help="Input image URL. Repeat for multi-image or keyframes.")
video.add_argument("--mode", choices=("ti2vid", "keyframes"))
video.add_argument("--height", type=int)
video.add_argument("--width", type=int)
video.add_argument("--num-frames", type=int, default=121)
video.add_argument("--frame-rate", type=float, default=24)
video.add_argument("--num-inference-steps", type=int)
video.add_argument("--seed", type=int)
video.add_argument("--negative-prompt")
video.add_argument(
"--no-translate-prompt",
action="store_true",
help="Do not translate non-English prompts before sending to the video API.",
)
video.add_argument("--poll", action="store_true")
video.add_argument("--timeout", type=int, default=900)
video.add_argument("--interval", type=int, default=10)
video.add_argument("--raw", action="store_true", help="Print the raw provider response.")
video.set_defaults(func=cmd_video)
video_get = sub.add_parser("video-get", help="Retrieve a video result by video_id, or by legacy task_id.")
video_get.add_argument("identifier", help="Prefer video_id. Legacy task_id is still accepted.")
video_get.add_argument("--model-name", default=VIDEO_MODEL, help="Model name for video_id result lookup.")
video_get.add_argument("--no-model-name", action="store_true", help="Do not pass model_name for video_id lookup.")
video_get.add_argument("--raw", action="store_true", help="Print the raw provider response.")
video_get.set_defaults(func=cmd_video_get)
smoke = sub.add_parser("smoke-test", help="Run live text, image, and video API tests.")
smoke.add_argument("--image-size", default="1024x768")
smoke.add_argument("--video-height", type=int)
smoke.add_argument("--video-width", type=int)
smoke.add_argument("--video-num-frames", type=int, default=81)
smoke.add_argument("--video-frame-rate", type=float, default=24)
smoke.add_argument("--include-image-edit", action="store_true", help="Also test image-to-image editing.")
smoke.add_argument("--strict-tools", action="store_true", help="Fail if the tool-calling response has no tool_calls.")
smoke.add_argument("--poll-video", action="store_true")
smoke.add_argument("--video-timeout", type=int, default=900)
smoke.add_argument("--video-interval", type=int, default=10)
smoke.add_argument(
"--video-case",
action="append",
choices=VIDEO_CASES,
help="Video case to test. Repeat to test multiple cases. Omit to skip video creation.",
)
smoke.set_defaults(func=cmd_smoke_test)
return parser
def main() -> None:
args = build_parser().parse_args()
args.func(args)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick Agnes AI Generation for Agnes-specific API integration rather than local markitdown document conversion or Spring Boot codegen skills.
FAQ
What APIs does Agnes AI Generation call?
Agnes AI Generation calls Agnes and Sapiens text, image, and video endpoints at apihub.agnes-ai.com, including Agnes Image, Agnes Video, and Agnes 2.0 Flash, via scripts/agnes_api.py.
Does Agnes AI Generation require an API key?
Agnes AI Generation requires an API key for apihub.agnes-ai.com calls, using scripts/agnes_api.py and references/api.md to configure endpoints, parameters, and response fields.
Is Agnes Ai Generation safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.