Skip to content

Conversation

@mmabrouk
Copy link
Member

@mmabrouk mmabrouk commented Feb 9, 2026

Summary

  • Adds a new REST API at /preview/ai/services/ with an MCP-shaped tool-call contract for AI-powered features
  • Implements the first tool: tools.agenta.api.refine_prompt — refines a prompt template by calling a deployed prompt in an internal Agenta org
  • Includes EE permission checks (EDIT_WORKFLOWS), rate limiting (10 burst / 30 per min), and input/output validation

Endpoints

Method Path Description
GET /preview/ai/services/status Returns enabled flag + available tools
POST /preview/ai/services/tools/call Executes a tool call

Architecture

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 returns enabled: false and tool calls return 503.

Files

Backend

  • api/oss/src/core/ai_services/ — DTOs, HTTP client, service layer
  • api/oss/src/apis/fastapi/ai_services/ — FastAPI router + models
  • api/oss/src/utils/env.pyAIServicesConfig
  • api/entrypoints/routers.py — wiring

Design docs

  • docs/design/ai-actions/ — spec, plan, context, research, status

What's next

  • Phase 2: Frontend integration (status query, API client, Refine prompt button in Playground)
  • Phase 3: Hardening (structured logging, trace_id propagation)

Open with Devin

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
@vercel
Copy link

vercel bot commented Feb 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Feb 11, 2026 9:02am

Request Review

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

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.
@mmabrouk mmabrouk requested a review from jp-agenta February 10, 2026 18:16
@mmabrouk mmabrouk marked this pull request as ready for review February 10, 2026 18:16
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. Backend labels Feb 10, 2026
Remove empty __init__.py file in ai_services module

@intercept_exceptions()
async def get_status(self, request: Request) -> AIServicesStatusResponse:
allow_tools = True
Copy link
Contributor

@junaway junaway Feb 11, 2026

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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"
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

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":
Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

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(
Copy link
Contributor

@junaway junaway Feb 11, 2026

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=(
Copy link
Contributor

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:
Copy link
Contributor

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(
Copy link
Contributor

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")
Copy link
Contributor

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants