redis-agent-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redis_agent_kit/__init__.py +222 -0
- redis_agent_kit/agent.py +265 -0
- redis_agent_kit/api/__init__.py +21 -0
- redis_agent_kit/api/app.py +163 -0
- redis_agent_kit/api/auth.py +97 -0
- redis_agent_kit/api/pipelines.py +240 -0
- redis_agent_kit/api/tasks.py +511 -0
- redis_agent_kit/api/v1.py +386 -0
- redis_agent_kit/attachments.py +225 -0
- redis_agent_kit/cli/__init__.py +5 -0
- redis_agent_kit/cli/agent.py +54 -0
- redis_agent_kit/cli/api_common.py +130 -0
- redis_agent_kit/cli/chat.py +434 -0
- redis_agent_kit/cli/health.py +21 -0
- redis_agent_kit/cli/main.py +173 -0
- redis_agent_kit/cli/pipelines.py +327 -0
- redis_agent_kit/cli/task.py +298 -0
- redis_agent_kit/cli/up.py +67 -0
- redis_agent_kit/cli/worker.py +103 -0
- redis_agent_kit/config.py +209 -0
- redis_agent_kit/core.py +575 -0
- redis_agent_kit/emitter.py +126 -0
- redis_agent_kit/keys.py +119 -0
- redis_agent_kit/knowledge.py +188 -0
- redis_agent_kit/mcp/__init__.py +5 -0
- redis_agent_kit/mcp/server.py +340 -0
- redis_agent_kit/memory.py +700 -0
- redis_agent_kit/middleware.py +481 -0
- redis_agent_kit/models.py +329 -0
- redis_agent_kit/pipelines/__init__.py +64 -0
- redis_agent_kit/pipelines/artifact_storage.py +189 -0
- redis_agent_kit/pipelines/config_loader.py +113 -0
- redis_agent_kit/pipelines/deduplicator.py +114 -0
- redis_agent_kit/pipelines/frontmatter.py +109 -0
- redis_agent_kit/pipelines/ingest_stage.py +100 -0
- redis_agent_kit/pipelines/models.py +209 -0
- redis_agent_kit/pipelines/orchestrator.py +285 -0
- redis_agent_kit/pipelines/pipeline.py +309 -0
- redis_agent_kit/pipelines/prepare_stage.py +160 -0
- redis_agent_kit/pipelines/processor.py +120 -0
- redis_agent_kit/pipelines/scraper.py +96 -0
- redis_agent_kit/pipelines/vector_store.py +386 -0
- redis_agent_kit/protocols/__init__.py +23 -0
- redis_agent_kit/protocols/a2a/__init__.py +35 -0
- redis_agent_kit/protocols/a2a/adapter.py +148 -0
- redis_agent_kit/protocols/a2a/models.py +101 -0
- redis_agent_kit/protocols/a2a/router.py +196 -0
- redis_agent_kit/protocols/acp/__init__.py +28 -0
- redis_agent_kit/protocols/acp/adapter.py +160 -0
- redis_agent_kit/protocols/acp/models.py +65 -0
- redis_agent_kit/protocols/acp/router.py +108 -0
- redis_agent_kit/protocols/base.py +134 -0
- redis_agent_kit/protocols/unified.py +111 -0
- redis_agent_kit/side_effects.py +381 -0
- redis_agent_kit/streaming.py +148 -0
- redis_agent_kit/task_manager.py +601 -0
- redis_agent_kit/thread_manager.py +176 -0
- redis_agent_kit/tools/__init__.py +33 -0
- redis_agent_kit/tools/decorator.py +553 -0
- redis_agent_kit/tools/knowledge_provider.py +82 -0
- redis_agent_kit/tools/manager.py +141 -0
- redis_agent_kit/tools/models.py +67 -0
- redis_agent_kit/tools/provider.py +106 -0
- redis_agent_kit/vectorizer.py +197 -0
- redis_agent_kit-0.1.0.dist-info/METADATA +198 -0
- redis_agent_kit-0.1.0.dist-info/RECORD +68 -0
- redis_agent_kit-0.1.0.dist-info/WHEEL +4 -0
- redis_agent_kit-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Redis Agent Kit - Reusable infrastructure for building AI agents with Redis."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
# Core (AgentKit handles background processing internally)
|
|
6
|
+
# Pipelines (import submodule for namespaced access)
|
|
7
|
+
# Tools
|
|
8
|
+
from redis_agent_kit import pipelines, tools
|
|
9
|
+
|
|
10
|
+
# Agent and Knowledge
|
|
11
|
+
from redis_agent_kit.agent import Agent, AgentConfig
|
|
12
|
+
|
|
13
|
+
# API Auth (optional – requires the [api] extra for FastAPI)
|
|
14
|
+
try:
|
|
15
|
+
from redis_agent_kit.api.auth import (
|
|
16
|
+
create_api_key_dependency,
|
|
17
|
+
create_optional_api_key_dependency,
|
|
18
|
+
)
|
|
19
|
+
except ImportError: # pragma: no cover - exercised only without [api]
|
|
20
|
+
create_api_key_dependency = None # type: ignore[assignment]
|
|
21
|
+
create_optional_api_key_dependency = None # type: ignore[assignment]
|
|
22
|
+
|
|
23
|
+
# Attachments
|
|
24
|
+
from redis_agent_kit.attachments import (
|
|
25
|
+
AttachmentStore,
|
|
26
|
+
InlineAttachmentStore,
|
|
27
|
+
RedisAttachmentStore,
|
|
28
|
+
get_attachment_data,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Config
|
|
32
|
+
from redis_agent_kit.config import (
|
|
33
|
+
MemorySettings,
|
|
34
|
+
ModelSettings,
|
|
35
|
+
Settings,
|
|
36
|
+
SideEffectSettings,
|
|
37
|
+
TaskSettings,
|
|
38
|
+
get_settings,
|
|
39
|
+
reset_settings,
|
|
40
|
+
set_settings,
|
|
41
|
+
)
|
|
42
|
+
from redis_agent_kit.core import (
|
|
43
|
+
AgentCallable,
|
|
44
|
+
AgentKit,
|
|
45
|
+
ConcurrencyConfig,
|
|
46
|
+
ContextAgentCallable,
|
|
47
|
+
RetryConfig,
|
|
48
|
+
create_task_with_session,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Emitter
|
|
52
|
+
from redis_agent_kit.emitter import TaskEmitter
|
|
53
|
+
|
|
54
|
+
# Keys
|
|
55
|
+
from redis_agent_kit.keys import RedisKeys
|
|
56
|
+
from redis_agent_kit.knowledge import KnowledgeStore
|
|
57
|
+
|
|
58
|
+
# Memory
|
|
59
|
+
from redis_agent_kit.memory import Memory, MemorySearchResults, MessageList, NoOpMemory
|
|
60
|
+
|
|
61
|
+
# Task Middleware
|
|
62
|
+
# Pipeline Middleware
|
|
63
|
+
from redis_agent_kit.middleware import (
|
|
64
|
+
EmitterMiddleware,
|
|
65
|
+
MemoryMiddleware,
|
|
66
|
+
MiddlewareChain,
|
|
67
|
+
PipelineContext,
|
|
68
|
+
PipelineLoggingMiddleware,
|
|
69
|
+
PipelineMiddleware,
|
|
70
|
+
PipelineMiddlewareChain,
|
|
71
|
+
PipelineTimingMiddleware,
|
|
72
|
+
PipelineValidationMiddleware,
|
|
73
|
+
RAGMiddleware,
|
|
74
|
+
ResultMiddleware,
|
|
75
|
+
TaskContext,
|
|
76
|
+
TaskMiddleware,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Models
|
|
80
|
+
from redis_agent_kit.models import (
|
|
81
|
+
AgentCard,
|
|
82
|
+
AgentManifest,
|
|
83
|
+
AgentMetadata,
|
|
84
|
+
AgentStatus,
|
|
85
|
+
Attachment,
|
|
86
|
+
AttachmentType,
|
|
87
|
+
Caller,
|
|
88
|
+
CallerType,
|
|
89
|
+
Capability,
|
|
90
|
+
InputRequest,
|
|
91
|
+
Message,
|
|
92
|
+
Skill,
|
|
93
|
+
TaskState,
|
|
94
|
+
TaskStatus,
|
|
95
|
+
TaskUpdate,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Side Effects (reentrant tasks)
|
|
99
|
+
from redis_agent_kit.side_effects import (
|
|
100
|
+
DEFAULT_KEY_POLICY,
|
|
101
|
+
FUNCTION_NAME,
|
|
102
|
+
FUNCTION_SOURCE,
|
|
103
|
+
INPUTS,
|
|
104
|
+
clear_side_effects,
|
|
105
|
+
side_effect,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Streaming
|
|
109
|
+
from redis_agent_kit.streaming import ChannelScope, StreamConfig
|
|
110
|
+
|
|
111
|
+
# Managers
|
|
112
|
+
from redis_agent_kit.task_manager import TaskManager
|
|
113
|
+
from redis_agent_kit.thread_manager import ThreadManager
|
|
114
|
+
from redis_agent_kit.tools import (
|
|
115
|
+
Tool,
|
|
116
|
+
ToolCapability,
|
|
117
|
+
ToolDefinition,
|
|
118
|
+
ToolManager,
|
|
119
|
+
ToolMetadata,
|
|
120
|
+
ToolProvider,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Vectorizer
|
|
124
|
+
from redis_agent_kit.vectorizer import LiteLLMVectorizerAdapter, Vectorizer
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
# Core
|
|
128
|
+
"AgentKit",
|
|
129
|
+
"AgentCallable",
|
|
130
|
+
"ContextAgentCallable",
|
|
131
|
+
"create_task_with_session",
|
|
132
|
+
# Task configuration helpers
|
|
133
|
+
"RetryConfig",
|
|
134
|
+
"ConcurrencyConfig",
|
|
135
|
+
# Config
|
|
136
|
+
"Settings",
|
|
137
|
+
"MemorySettings",
|
|
138
|
+
"TaskSettings",
|
|
139
|
+
"ModelSettings",
|
|
140
|
+
"SideEffectSettings",
|
|
141
|
+
"get_settings",
|
|
142
|
+
"set_settings",
|
|
143
|
+
"reset_settings",
|
|
144
|
+
# Side Effects (reentrant tasks)
|
|
145
|
+
"side_effect",
|
|
146
|
+
"clear_side_effects",
|
|
147
|
+
"FUNCTION_SOURCE",
|
|
148
|
+
"FUNCTION_NAME",
|
|
149
|
+
"INPUTS",
|
|
150
|
+
"DEFAULT_KEY_POLICY",
|
|
151
|
+
# Memory
|
|
152
|
+
"Memory",
|
|
153
|
+
"MessageList",
|
|
154
|
+
"MemorySearchResults",
|
|
155
|
+
"NoOpMemory",
|
|
156
|
+
# Managers
|
|
157
|
+
"TaskManager",
|
|
158
|
+
"ThreadManager",
|
|
159
|
+
# Emitter
|
|
160
|
+
"TaskEmitter",
|
|
161
|
+
# Task Middleware
|
|
162
|
+
"TaskMiddleware",
|
|
163
|
+
"TaskContext",
|
|
164
|
+
"MiddlewareChain",
|
|
165
|
+
"EmitterMiddleware",
|
|
166
|
+
"RAGMiddleware",
|
|
167
|
+
"MemoryMiddleware",
|
|
168
|
+
"ResultMiddleware",
|
|
169
|
+
# Pipeline Middleware
|
|
170
|
+
"PipelineMiddleware",
|
|
171
|
+
"PipelineContext",
|
|
172
|
+
"PipelineMiddlewareChain",
|
|
173
|
+
"PipelineLoggingMiddleware",
|
|
174
|
+
"PipelineTimingMiddleware",
|
|
175
|
+
"PipelineValidationMiddleware",
|
|
176
|
+
# Models
|
|
177
|
+
"AgentCard",
|
|
178
|
+
"AgentManifest",
|
|
179
|
+
"AgentMetadata",
|
|
180
|
+
"AgentStatus",
|
|
181
|
+
"Attachment",
|
|
182
|
+
"AttachmentType",
|
|
183
|
+
"Caller",
|
|
184
|
+
"CallerType",
|
|
185
|
+
"Capability",
|
|
186
|
+
"InputRequest",
|
|
187
|
+
"Message",
|
|
188
|
+
"Skill",
|
|
189
|
+
"TaskState",
|
|
190
|
+
"TaskStatus",
|
|
191
|
+
# Attachment Storage
|
|
192
|
+
"AttachmentStore",
|
|
193
|
+
"RedisAttachmentStore",
|
|
194
|
+
"InlineAttachmentStore",
|
|
195
|
+
"get_attachment_data",
|
|
196
|
+
"TaskUpdate",
|
|
197
|
+
# Keys
|
|
198
|
+
"RedisKeys",
|
|
199
|
+
# API Auth
|
|
200
|
+
"create_api_key_dependency",
|
|
201
|
+
"create_optional_api_key_dependency",
|
|
202
|
+
# Pipelines submodule
|
|
203
|
+
"pipelines",
|
|
204
|
+
# Agent and Knowledge
|
|
205
|
+
"Agent",
|
|
206
|
+
"AgentConfig",
|
|
207
|
+
"KnowledgeStore",
|
|
208
|
+
# Vectorizer
|
|
209
|
+
"Vectorizer",
|
|
210
|
+
"LiteLLMVectorizerAdapter",
|
|
211
|
+
# Tools
|
|
212
|
+
"tools",
|
|
213
|
+
"Tool",
|
|
214
|
+
"ToolCapability",
|
|
215
|
+
"ToolDefinition",
|
|
216
|
+
"ToolManager",
|
|
217
|
+
"ToolMetadata",
|
|
218
|
+
"ToolProvider",
|
|
219
|
+
# Streaming
|
|
220
|
+
"StreamConfig",
|
|
221
|
+
"ChannelScope",
|
|
222
|
+
]
|
redis_agent_kit/agent.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Framework-agnostic Agent base class using LiteLLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import litellm
|
|
11
|
+
import redis.asyncio as redis
|
|
12
|
+
|
|
13
|
+
from redis_agent_kit.knowledge import KnowledgeStore
|
|
14
|
+
from redis_agent_kit.tools import ToolManager, ToolProvider
|
|
15
|
+
from redis_agent_kit.tools.knowledge_provider import KnowledgeProvider
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Message:
|
|
22
|
+
"""A message in the conversation."""
|
|
23
|
+
|
|
24
|
+
role: str # "system", "user", "assistant", "tool"
|
|
25
|
+
content: str | None = None
|
|
26
|
+
tool_calls: list[Any] | None = None
|
|
27
|
+
tool_call_id: str | None = None
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
"""Convert to dict for LiteLLM."""
|
|
31
|
+
d: dict[str, Any] = {"role": self.role}
|
|
32
|
+
if self.content is not None:
|
|
33
|
+
d["content"] = self.content
|
|
34
|
+
if self.tool_calls:
|
|
35
|
+
d["tool_calls"] = self.tool_calls
|
|
36
|
+
if self.tool_call_id:
|
|
37
|
+
d["tool_call_id"] = self.tool_call_id
|
|
38
|
+
return d
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class AgentConfig:
|
|
43
|
+
"""Configuration for Agent."""
|
|
44
|
+
|
|
45
|
+
model: str = "gpt-4o-mini"
|
|
46
|
+
system_prompt: str | None = None
|
|
47
|
+
temperature: float = 0.7
|
|
48
|
+
max_tokens: int | None = None
|
|
49
|
+
max_tool_iterations: int = 10
|
|
50
|
+
use_knowledge: bool = True
|
|
51
|
+
use_tools: bool = True
|
|
52
|
+
knowledge_context_limit: int = 5
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Agent:
|
|
56
|
+
"""Framework-agnostic agent with LiteLLM backend.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
agent = Agent(
|
|
60
|
+
model="gpt-4o-mini", # Or "anthropic/claude-3-5-sonnet"
|
|
61
|
+
redis_url="redis://localhost:6379",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Ingest knowledge
|
|
65
|
+
await agent.knowledge.ingest("Redis uses RDB for persistence...")
|
|
66
|
+
|
|
67
|
+
# Add custom tools
|
|
68
|
+
agent.tools.add_provider(MyProvider())
|
|
69
|
+
|
|
70
|
+
# Chat
|
|
71
|
+
response = await agent.chat("How does Redis persistence work?")
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
model: str = "gpt-4o-mini",
|
|
77
|
+
system_prompt: str | None = None,
|
|
78
|
+
redis_url: str = "redis://localhost:6379",
|
|
79
|
+
redis_client: redis.Redis | None = None,
|
|
80
|
+
config: AgentConfig | None = None,
|
|
81
|
+
):
|
|
82
|
+
"""Initialize agent.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
model: LiteLLM model name (e.g., "gpt-4o-mini", "anthropic/claude-3-5-sonnet").
|
|
86
|
+
system_prompt: Optional system prompt.
|
|
87
|
+
redis_url: Redis connection URL.
|
|
88
|
+
redis_client: Optional existing Redis client.
|
|
89
|
+
config: Optional full configuration.
|
|
90
|
+
"""
|
|
91
|
+
self._config = config or AgentConfig(model=model, system_prompt=system_prompt)
|
|
92
|
+
self._client = redis_client or redis.from_url(redis_url, decode_responses=True) # type: ignore[no-untyped-call]
|
|
93
|
+
|
|
94
|
+
# Core components
|
|
95
|
+
self.knowledge = KnowledgeStore(self._client)
|
|
96
|
+
self.tools = ToolManager()
|
|
97
|
+
|
|
98
|
+
# Register built-in knowledge tool
|
|
99
|
+
self._knowledge_provider = KnowledgeProvider(self.knowledge)
|
|
100
|
+
self.tools.add_provider(self._knowledge_provider)
|
|
101
|
+
|
|
102
|
+
# Conversation history
|
|
103
|
+
self._history: list[Message] = []
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def model(self) -> str:
|
|
107
|
+
"""Get the model name."""
|
|
108
|
+
return self._config.model
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def config(self) -> AgentConfig:
|
|
112
|
+
"""Get agent configuration."""
|
|
113
|
+
return self._config
|
|
114
|
+
|
|
115
|
+
def add_provider(self, provider: ToolProvider) -> None:
|
|
116
|
+
"""Add a tool provider.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
provider: ToolProvider instance.
|
|
120
|
+
"""
|
|
121
|
+
self.tools.add_provider(provider)
|
|
122
|
+
|
|
123
|
+
def clear_history(self) -> None:
|
|
124
|
+
"""Clear conversation history."""
|
|
125
|
+
self._history.clear()
|
|
126
|
+
|
|
127
|
+
async def chat(
|
|
128
|
+
self,
|
|
129
|
+
message: str,
|
|
130
|
+
use_knowledge: bool | None = None,
|
|
131
|
+
use_tools: bool | None = None,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Send a message and get a response.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
message: User message.
|
|
137
|
+
use_knowledge: Override config to enable/disable knowledge context.
|
|
138
|
+
use_tools: Override config to enable/disable tools.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Assistant response.
|
|
142
|
+
"""
|
|
143
|
+
use_knowledge = use_knowledge if use_knowledge is not None else self._config.use_knowledge
|
|
144
|
+
use_tools = use_tools if use_tools is not None else self._config.use_tools
|
|
145
|
+
|
|
146
|
+
messages = self._build_messages(message, use_knowledge=use_knowledge)
|
|
147
|
+
|
|
148
|
+
# Get tools
|
|
149
|
+
tools = None
|
|
150
|
+
if use_tools and self.tools._tools:
|
|
151
|
+
tools = self.tools.get_openai_tools()
|
|
152
|
+
|
|
153
|
+
# Call LLM
|
|
154
|
+
response = await self._call_llm(messages, tools)
|
|
155
|
+
|
|
156
|
+
# Handle tool calls
|
|
157
|
+
iterations = 0
|
|
158
|
+
while (
|
|
159
|
+
response.choices[0].message.tool_calls and iterations < self._config.max_tool_iterations
|
|
160
|
+
):
|
|
161
|
+
response = await self._handle_tool_calls(response, messages)
|
|
162
|
+
iterations += 1
|
|
163
|
+
|
|
164
|
+
# Extract response
|
|
165
|
+
assistant_content = response.choices[0].message.content or ""
|
|
166
|
+
|
|
167
|
+
# Store in history
|
|
168
|
+
self._history.append(Message(role="user", content=message))
|
|
169
|
+
self._history.append(Message(role="assistant", content=assistant_content))
|
|
170
|
+
|
|
171
|
+
return assistant_content
|
|
172
|
+
|
|
173
|
+
def _build_messages(
|
|
174
|
+
self,
|
|
175
|
+
user_message: str,
|
|
176
|
+
use_knowledge: bool = True,
|
|
177
|
+
) -> list[dict[str, Any]]:
|
|
178
|
+
"""Build message list for LLM call."""
|
|
179
|
+
messages: list[dict[str, Any]] = []
|
|
180
|
+
|
|
181
|
+
# System prompt
|
|
182
|
+
if self._config.system_prompt:
|
|
183
|
+
messages.append({"role": "system", "content": self._config.system_prompt})
|
|
184
|
+
|
|
185
|
+
# History
|
|
186
|
+
for msg in self._history:
|
|
187
|
+
messages.append(msg.to_dict())
|
|
188
|
+
|
|
189
|
+
# User message
|
|
190
|
+
messages.append({"role": "user", "content": user_message})
|
|
191
|
+
|
|
192
|
+
return messages
|
|
193
|
+
|
|
194
|
+
async def _call_llm(
|
|
195
|
+
self,
|
|
196
|
+
messages: list[dict[str, Any]],
|
|
197
|
+
tools: list[dict[str, Any]] | None = None,
|
|
198
|
+
) -> Any:
|
|
199
|
+
"""Call LLM via LiteLLM."""
|
|
200
|
+
kwargs: dict[str, Any] = {
|
|
201
|
+
"model": self._config.model,
|
|
202
|
+
"messages": messages,
|
|
203
|
+
"temperature": self._config.temperature,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if self._config.max_tokens:
|
|
207
|
+
kwargs["max_tokens"] = self._config.max_tokens
|
|
208
|
+
|
|
209
|
+
if tools:
|
|
210
|
+
kwargs["tools"] = tools
|
|
211
|
+
|
|
212
|
+
return await litellm.acompletion(**kwargs)
|
|
213
|
+
|
|
214
|
+
async def _handle_tool_calls(
|
|
215
|
+
self,
|
|
216
|
+
response: Any,
|
|
217
|
+
messages: list[dict[str, Any]],
|
|
218
|
+
) -> Any:
|
|
219
|
+
"""Execute tool calls and continue conversation."""
|
|
220
|
+
assistant_msg = response.choices[0].message
|
|
221
|
+
|
|
222
|
+
# Add assistant message with tool calls
|
|
223
|
+
messages.append(
|
|
224
|
+
{
|
|
225
|
+
"role": "assistant",
|
|
226
|
+
"content": assistant_msg.content,
|
|
227
|
+
"tool_calls": [
|
|
228
|
+
{
|
|
229
|
+
"id": tc.id,
|
|
230
|
+
"type": "function",
|
|
231
|
+
"function": {
|
|
232
|
+
"name": tc.function.name,
|
|
233
|
+
"arguments": tc.function.arguments,
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
for tc in assistant_msg.tool_calls
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Execute each tool call
|
|
242
|
+
for tool_call in assistant_msg.tool_calls:
|
|
243
|
+
tool_name = tool_call.function.name
|
|
244
|
+
try:
|
|
245
|
+
args = json.loads(tool_call.function.arguments)
|
|
246
|
+
except json.JSONDecodeError:
|
|
247
|
+
args = {}
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
result = await self.tools.execute(tool_name, args)
|
|
251
|
+
result_str = json.dumps(result) if not isinstance(result, str) else result
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.warning(f"Tool {tool_name} failed: {e}")
|
|
254
|
+
result_str = json.dumps({"error": str(e)})
|
|
255
|
+
|
|
256
|
+
messages.append(
|
|
257
|
+
{
|
|
258
|
+
"role": "tool",
|
|
259
|
+
"tool_call_id": tool_call.id,
|
|
260
|
+
"content": result_str,
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Continue conversation with tool results
|
|
265
|
+
return await self._call_llm(messages, self.tools.get_openai_tools())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Redis Agent Kit REST API.
|
|
2
|
+
|
|
3
|
+
The REST API requires the ``[api]`` extra (FastAPI + Uvicorn). ``create_app``
|
|
4
|
+
is loaded lazily so that importing :mod:`redis_agent_kit.api` on a bare
|
|
5
|
+
install does not eagerly fail when FastAPI is not installed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["create_app"]
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
from redis_agent_kit.api.app import create_app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str) -> Any:
|
|
17
|
+
if name == "create_app":
|
|
18
|
+
from redis_agent_kit.api.app import create_app as _create_app
|
|
19
|
+
|
|
20
|
+
return _create_app
|
|
21
|
+
raise AttributeError(f"module 'redis_agent_kit.api' has no attribute {name!r}")
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Redis Agent Kit REST API application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, FastAPI
|
|
8
|
+
|
|
9
|
+
from redis_agent_kit import __version__
|
|
10
|
+
from redis_agent_kit.api.auth import create_optional_api_key_dependency
|
|
11
|
+
from redis_agent_kit.api.pipelines import router as pipelines_router
|
|
12
|
+
from redis_agent_kit.api.tasks import router as tasks_router
|
|
13
|
+
from redis_agent_kit.api.v1 import router as v1_router
|
|
14
|
+
from redis_agent_kit.config import get_settings
|
|
15
|
+
from redis_agent_kit.models import AgentCard, AgentManifest
|
|
16
|
+
from redis_agent_kit.protocols.a2a import create_a2a_router
|
|
17
|
+
from redis_agent_kit.protocols.acp import create_acp_router
|
|
18
|
+
from redis_agent_kit.streaming import StreamConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_app(
|
|
22
|
+
redis_url: str | None = None,
|
|
23
|
+
prefix: str = "rak",
|
|
24
|
+
queue_name: str | None = None,
|
|
25
|
+
enable_a2a: bool = False,
|
|
26
|
+
enable_acp: bool = False,
|
|
27
|
+
enable_sse: bool = False,
|
|
28
|
+
enable_streaming: bool = False,
|
|
29
|
+
stream_config: StreamConfig | None = None,
|
|
30
|
+
agent_card: AgentCard | None = None,
|
|
31
|
+
agent_manifest: AgentManifest | None = None,
|
|
32
|
+
kit: Any = None,
|
|
33
|
+
api_keys: list[str] | None = None,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
) -> FastAPI:
|
|
36
|
+
"""Create and configure the FastAPI application.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
redis_url: Redis connection URL (defaults to settings).
|
|
40
|
+
prefix: Redis key prefix.
|
|
41
|
+
queue_name: Background worker queue name (defaults to settings).
|
|
42
|
+
enable_a2a: Enable A2A protocol endpoints.
|
|
43
|
+
enable_acp: Enable ACP protocol endpoints.
|
|
44
|
+
enable_sse: Enable Server-Sent Events streaming endpoint for tasks.
|
|
45
|
+
Deprecated – use ``stream_config`` instead.
|
|
46
|
+
enable_streaming: Enable lightweight Pub/Sub token streaming.
|
|
47
|
+
Deprecated – use ``stream_config`` instead.
|
|
48
|
+
stream_config: Structured streaming configuration. When provided,
|
|
49
|
+
``enable_sse`` and ``enable_streaming`` are ignored.
|
|
50
|
+
agent_card: Agent card for A2A discovery (required if enable_a2a=True).
|
|
51
|
+
agent_manifest: Agent manifest for ACP discovery (required if enable_acp=True).
|
|
52
|
+
kit: AgentKit instance for protocol handlers (required if protocols enabled).
|
|
53
|
+
api_keys: Optional list of valid API keys. If provided, all endpoints
|
|
54
|
+
(except /, /health, /docs) require authentication via X-API-Key
|
|
55
|
+
header or Authorization: Bearer token.
|
|
56
|
+
**kwargs: Additional arguments passed to FastAPI constructor.
|
|
57
|
+
Common options include:
|
|
58
|
+
- title: API title (default: "Redis Agent Kit API")
|
|
59
|
+
- description: API description
|
|
60
|
+
- version: API version (default: package version)
|
|
61
|
+
- docs_url: URL for Swagger docs (default: "/docs")
|
|
62
|
+
- redoc_url: URL for ReDoc (default: "/redoc")
|
|
63
|
+
- openapi_url: URL for OpenAPI schema (default: "/openapi.json")
|
|
64
|
+
- middleware: List of middleware to add
|
|
65
|
+
- dependencies: Global dependencies
|
|
66
|
+
- lifespan: Lifespan context manager
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Configured FastAPI application.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If protocol is enabled but required config is missing.
|
|
73
|
+
"""
|
|
74
|
+
# Validate protocol configuration
|
|
75
|
+
if enable_a2a and agent_card is None:
|
|
76
|
+
raise ValueError("agent_card is required when A2A is enabled")
|
|
77
|
+
if enable_acp and agent_manifest is None:
|
|
78
|
+
raise ValueError("agent_manifest is required when ACP is enabled")
|
|
79
|
+
if (enable_a2a or enable_acp) and kit is None:
|
|
80
|
+
raise ValueError("kit is required when protocols are enabled")
|
|
81
|
+
|
|
82
|
+
settings = get_settings()
|
|
83
|
+
resolved_redis_url = redis_url or settings.redis_url
|
|
84
|
+
resolved_queue_name = queue_name or settings.task.queue_name
|
|
85
|
+
|
|
86
|
+
# Create API key dependency if keys provided
|
|
87
|
+
auth_dependency = create_optional_api_key_dependency(api_keys)
|
|
88
|
+
|
|
89
|
+
# Set defaults that can be overridden by kwargs
|
|
90
|
+
fastapi_kwargs: dict[str, Any] = {
|
|
91
|
+
"title": "Redis Agent Kit API",
|
|
92
|
+
"description": "REST API for managing agent tasks and data pipelines",
|
|
93
|
+
"version": __version__,
|
|
94
|
+
}
|
|
95
|
+
fastapi_kwargs.update(kwargs)
|
|
96
|
+
|
|
97
|
+
app = FastAPI(**fastapi_kwargs)
|
|
98
|
+
|
|
99
|
+
# Resolve StreamConfig
|
|
100
|
+
if stream_config is not None:
|
|
101
|
+
resolved_stream_config = stream_config
|
|
102
|
+
else:
|
|
103
|
+
resolved_stream_config = StreamConfig.from_booleans(enable_sse, enable_streaming)
|
|
104
|
+
|
|
105
|
+
# Store config in app state
|
|
106
|
+
app.state.redis_url = resolved_redis_url
|
|
107
|
+
app.state.prefix = prefix
|
|
108
|
+
app.state.queue_name = resolved_queue_name
|
|
109
|
+
app.state.kit = kit
|
|
110
|
+
app.state.api_keys = api_keys
|
|
111
|
+
app.state.enable_a2a = enable_a2a
|
|
112
|
+
app.state.enable_acp = enable_acp
|
|
113
|
+
app.state.enable_mcp = bool(kwargs.get("enable_mcp", False))
|
|
114
|
+
app.state.default_agent_id = kwargs.get("default_agent_id", "default")
|
|
115
|
+
app.state.stream_config = resolved_stream_config
|
|
116
|
+
app.state.enable_sse = enable_sse
|
|
117
|
+
app.state.enable_streaming = enable_streaming
|
|
118
|
+
|
|
119
|
+
# Register routes (public - no auth required)
|
|
120
|
+
@app.get("/")
|
|
121
|
+
async def root() -> dict[str, str]:
|
|
122
|
+
"""Get API information."""
|
|
123
|
+
return {
|
|
124
|
+
"name": "Redis Agent Kit API",
|
|
125
|
+
"version": __version__,
|
|
126
|
+
"docs": "/docs",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@app.get("/health")
|
|
130
|
+
async def health() -> dict[str, str]:
|
|
131
|
+
"""Health check endpoint."""
|
|
132
|
+
return {"status": "healthy"}
|
|
133
|
+
|
|
134
|
+
# Build dependencies list for protected routers
|
|
135
|
+
router_dependencies = [Depends(auth_dependency)] if auth_dependency else []
|
|
136
|
+
|
|
137
|
+
# Register routers (protected if api_keys provided)
|
|
138
|
+
app.include_router(
|
|
139
|
+
tasks_router, prefix="/tasks", tags=["tasks"], dependencies=router_dependencies
|
|
140
|
+
)
|
|
141
|
+
app.include_router(
|
|
142
|
+
pipelines_router,
|
|
143
|
+
prefix="/pipelines",
|
|
144
|
+
tags=["pipelines"],
|
|
145
|
+
dependencies=router_dependencies,
|
|
146
|
+
)
|
|
147
|
+
app.include_router(
|
|
148
|
+
v1_router,
|
|
149
|
+
tags=["v1"],
|
|
150
|
+
dependencies=router_dependencies,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Mount A2A router if enabled (protected if api_keys provided)
|
|
154
|
+
if enable_a2a and agent_card is not None and kit is not None:
|
|
155
|
+
a2a_router = create_a2a_router(agent_card=agent_card, kit=kit)
|
|
156
|
+
app.include_router(a2a_router, tags=["A2A"], dependencies=router_dependencies)
|
|
157
|
+
|
|
158
|
+
# Mount ACP router if enabled (protected if api_keys provided)
|
|
159
|
+
if enable_acp and agent_manifest is not None and kit is not None:
|
|
160
|
+
acp_router = create_acp_router(agent_manifest=agent_manifest, kit=kit)
|
|
161
|
+
app.include_router(acp_router, tags=["ACP"], dependencies=router_dependencies)
|
|
162
|
+
|
|
163
|
+
return app
|