-
Notifications
You must be signed in to change notification settings - Fork 478
feat(api): AI services backend — refine prompt tool call #3687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Implement the backend for Chapter 1 of the AI services feature: - REST API with MCP-shaped tool-call contract at /preview/ai/services/ - Single tool: tools.agenta.api.refine_prompt - Thin HTTP client calling deployed prompt in internal Agenta org - EE permission check (EDIT_WORKFLOWS) and rate limiting - Input/output validation for prompt templates - Design docs with spec, plan, context, and research
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The design reference doc used 'prompt_text' as the input key while the service code, DTOs, spec, and input schema all use 'prompt_template_json'. Align the doc to match.
Remove empty __init__.py file in ai_services module
|
|
||
| @intercept_exceptions() | ||
| async def get_status(self, request: Request) -> AIServicesStatusResponse: | ||
| allow_tools = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this meant to become an env var or a feat flag ?
| allow_tools = await check_action_access( # type: ignore | ||
| user_uid=request.state.user_id, | ||
| project_id=request.state.project_id, | ||
| permission=Permission.EDIT_WORKFLOWS, # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all tools meant to mutate workflows ?
Should we have VIEW_AI_SERVICES and RUN_AI_SERVICES like we have for RUN_SERVICES, and then leave specific entitlements to specific tools ?
| ): | ||
| raise FORBIDDEN_EXCEPTION # type: ignore | ||
|
|
||
| # Router-level rate limit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity,
(1) why not via entitlements ?
(2) why not via the middleware ?
| headers={"Retry-After": str(retry_after)}, | ||
| ) | ||
|
|
||
| # Tool routing + strict request validation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eventually, we might want to push this down to the dispatcher, which would generate a domain-level exception, caught here and turned into an HTTP exception.
Removed the docstring for the AI Services core module.
| Returns: (raw_response, trace_id) | ||
| """ | ||
|
|
||
| url = f"{self.api_url}/services/completion/run" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This turns into {BASE_URL}/api/services/{SERVICE_PATH} instead of {BASE_URL}/services/{SERVICE_PATH}, no ?
| url=url, | ||
| ) | ||
| # Surface as tool execution error (caller maps to isError) | ||
| return { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to create domain-level exceptions via Pydantic models and then raise exception. There are some examples of this throughout the codebase (not enough IMO).
| if isinstance(data, dict): | ||
| trace_id = data.get("trace_id") or data.get("traceId") | ||
|
|
||
| return data, trace_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing for the returns data via dtos.
|
|
||
| class AIServicesService: | ||
| @classmethod | ||
| def from_env(cls) -> "AIServicesService": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to steal this idea of .from_env() clean up the not-yet-inverted dependency to env vars. Thanks !
| class AIServicesService: | ||
| @classmethod | ||
| def from_env(cls) -> "AIServicesService": | ||
| config = env.ai_services |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd avoid this intermediate variables, for readability.
|
|
||
| # enabled implies these exist, but keep this defensive. | ||
| if not api_url or not api_key: | ||
| return cls(config=config, client=None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're still coupling to structure of env vars and the settings in the service, unless you de-structure the config dict. Not a big problem, though, just flagging it.
| if not api_url or not api_key: | ||
| return cls(config=config, client=None) | ||
|
|
||
| client = AgentaAIServicesClient( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice dependency injection.
A purist would move this to adapters, have an interface for it, and would have this as the first implementation.
| ToolDefinition( | ||
| name=TOOL_REFINE_PROMPT, | ||
| title="Refine Prompt", | ||
| description=( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a _REFINE_PROMPT_HEADER_NAME/DESCRIPTION ?
| async def call_tool( | ||
| self, *, name: str, arguments: Dict[str, Any] | ||
| ) -> ToolCallResponse: | ||
| if name != TOOL_REFINE_PROMPT: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, here it is.
Double defense.
| ], | ||
| ) | ||
|
|
||
| async def call_tool( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this grow, honestly not too far in the future, this will probably turn into nested dispatchers:
tools.agenta.api.refine_prompt turns into:
dispatch to agenta tools handler
dispatch to api tools handler
dispatch to refine_prompt handler
|
|
||
| api_key: str | None = os.getenv("AGENTA_AI_SERVICES_API_KEY") | ||
| api_url: str | None = os.getenv("AGENTA_AI_SERVICES_API_URL") | ||
| environment: str | None = os.getenv("AGENTA_AI_SERVICES_ENVIRONMENT") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These names would deserve some love:
- ENVIRONMENT_SLUG / REFINE_PROMPT_KEY [recommended] (to match the preview entities)
- ENVIRONMENT_NAME / APP_SLUG (to match the legacy entities)
Summary
/preview/ai/services/with an MCP-shaped tool-call contract for AI-powered featurestools.agenta.api.refine_prompt— refines a prompt template by calling a deployed prompt in an internal Agenta orgEDIT_WORKFLOWS), rate limiting (10 burst / 30 per min), and input/output validationEndpoints
GET/preview/ai/services/statusPOST/preview/ai/services/tools/callArchitecture
Backend calls a deployed prompt in an internal Agenta org via
POST {API_URL}/services/completion/run. No direct LLM provider calls — Bedrock credentials live in the internal app config.Feature is env-var gated (
AGENTA_AI_SERVICES_*). When not configured, status returnsenabled: falseand tool calls return 503.Files
Backend
api/oss/src/core/ai_services/— DTOs, HTTP client, service layerapi/oss/src/apis/fastapi/ai_services/— FastAPI router + modelsapi/oss/src/utils/env.py—AIServicesConfigapi/entrypoints/routers.py— wiringDesign docs
docs/design/ai-actions/— spec, plan, context, research, statusWhat's next