mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-22 05:28:02 -05:00
Compare commits
12 Commits
fix/pgvect
...
swiftyos/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fed206fdc0 | ||
|
|
40ef2d511f | ||
|
|
20f4e14b2d | ||
|
|
1aee738f89 | ||
|
|
8b983cf2dc | ||
|
|
153c3df03a | ||
|
|
999b397a87 | ||
|
|
a79d19247c | ||
|
|
74ed697239 | ||
|
|
47b7470c36 | ||
|
|
3f6649a892 | ||
|
|
6572dcdb4f |
@@ -45,6 +45,9 @@ class StreamChatRequest(BaseModel):
|
||||
message: str
|
||||
is_user_message: bool = True
|
||||
context: dict[str, str] | None = None # {url: str, content: str}
|
||||
tags: list[str] | None = (
|
||||
None # Custom tags for Langfuse tracing (e.g., experiment names)
|
||||
)
|
||||
|
||||
|
||||
class CreateSessionResponse(BaseModel):
|
||||
@@ -229,6 +232,7 @@ async def stream_chat_post(
|
||||
user_id=user_id,
|
||||
session=session, # Pass pre-fetched session to avoid double-fetch
|
||||
context=request.context,
|
||||
tags=request.tags,
|
||||
):
|
||||
yield chunk.to_sse()
|
||||
# AI SDK protocol termination
|
||||
|
||||
@@ -63,7 +63,7 @@ def _is_langfuse_configured() -> bool:
|
||||
)
|
||||
|
||||
|
||||
async def _build_system_prompt(user_id: str | None) -> tuple[str, Any]:
|
||||
async def _build_system_prompt(user_id: str | None) -> tuple[str, Any, Any]:
|
||||
"""Build the full system prompt including business understanding if available.
|
||||
|
||||
Args:
|
||||
@@ -71,7 +71,7 @@ async def _build_system_prompt(user_id: str | None) -> tuple[str, Any]:
|
||||
If "default" and this is the user's first session, will use "onboarding" instead.
|
||||
|
||||
Returns:
|
||||
Tuple of (compiled prompt string, Langfuse prompt object for tracing)
|
||||
Tuple of (compiled prompt string, understanding object, Langfuse prompt object for tracing)
|
||||
"""
|
||||
|
||||
# cache_ttl_seconds=0 disables SDK caching to always get the latest prompt
|
||||
@@ -91,7 +91,7 @@ async def _build_system_prompt(user_id: str | None) -> tuple[str, Any]:
|
||||
context = "This is the first time you are meeting the user. Greet them and introduce them to the platform"
|
||||
|
||||
compiled = prompt.compile(users_information=context)
|
||||
return compiled, understanding
|
||||
return compiled, understanding, prompt
|
||||
|
||||
|
||||
async def _generate_session_title(message: str) -> str | None:
|
||||
@@ -156,6 +156,7 @@ async def stream_chat_completion(
|
||||
retry_count: int = 0,
|
||||
session: ChatSession | None = None,
|
||||
context: dict[str, str] | None = None, # {url: str, content: str}
|
||||
tags: list[str] | None = None, # Custom tags for Langfuse tracing
|
||||
) -> AsyncGenerator[StreamBaseResponse, None]:
|
||||
"""Main entry point for streaming chat completions with database handling.
|
||||
|
||||
@@ -265,7 +266,7 @@ async def stream_chat_completion(
|
||||
asyncio.create_task(_update_title())
|
||||
|
||||
# Build system prompt with business understanding
|
||||
system_prompt, understanding = await _build_system_prompt(user_id)
|
||||
system_prompt, understanding, langfuse_prompt = await _build_system_prompt(user_id)
|
||||
|
||||
# Create Langfuse trace for this LLM call (each call gets its own trace, grouped by session_id)
|
||||
# Using v3 SDK: start_observation creates a root span, update_trace sets trace-level attributes
|
||||
@@ -279,10 +280,15 @@ async def stream_chat_completion(
|
||||
name="user-copilot-request",
|
||||
input=input,
|
||||
) as span:
|
||||
# Merge custom tags with default "copilot" tag
|
||||
all_tags = ["copilot"]
|
||||
if tags:
|
||||
all_tags.extend(tags)
|
||||
|
||||
with propagate_attributes(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
tags=["copilot"],
|
||||
tags=all_tags,
|
||||
metadata={
|
||||
"users_information": format_understanding_for_prompt(understanding)[
|
||||
:200
|
||||
@@ -321,6 +327,7 @@ async def stream_chat_completion(
|
||||
tools=tools,
|
||||
system_prompt=system_prompt,
|
||||
text_block_id=text_block_id,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
):
|
||||
|
||||
if isinstance(chunk, StreamTextStart):
|
||||
@@ -467,6 +474,7 @@ async def stream_chat_completion(
|
||||
retry_count=retry_count + 1,
|
||||
session=session,
|
||||
context=context,
|
||||
tags=tags,
|
||||
):
|
||||
yield chunk
|
||||
return # Exit after retry to avoid double-saving in finally block
|
||||
@@ -516,6 +524,7 @@ async def stream_chat_completion(
|
||||
session=session, # Pass session object to avoid Redis refetch
|
||||
context=context,
|
||||
tool_call_response=str(tool_response_messages),
|
||||
tags=tags,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
@@ -534,8 +543,8 @@ def _is_retryable_error(error: Exception) -> bool:
|
||||
return True
|
||||
if isinstance(error, APIStatusError):
|
||||
# APIStatusError has a response with status_code
|
||||
# Retry on 5xx status codes (server errors)
|
||||
if error.response.status_code >= 500:
|
||||
# Retry on 5xx status codes (server errors) or 429 (rate limit)
|
||||
if error.response.status_code >= 500 or error.response.status_code == 429:
|
||||
return True
|
||||
if isinstance(error, APIError):
|
||||
# Retry on overloaded errors or 500 errors (may not have status code)
|
||||
@@ -550,6 +559,7 @@ async def _stream_chat_chunks(
|
||||
tools: list[ChatCompletionToolParam],
|
||||
system_prompt: str | None = None,
|
||||
text_block_id: str | None = None,
|
||||
langfuse_prompt: Any | None = None,
|
||||
) -> AsyncGenerator[StreamBaseResponse, None]:
|
||||
"""
|
||||
Pure streaming function for OpenAI chat completions with tool calling.
|
||||
@@ -561,6 +571,7 @@ async def _stream_chat_chunks(
|
||||
session: Chat session with conversation history
|
||||
tools: Available tools for the model
|
||||
system_prompt: System prompt to prepend to messages
|
||||
langfuse_prompt: Langfuse prompt object for linking to traces
|
||||
|
||||
Yields:
|
||||
SSE formatted JSON response objects
|
||||
@@ -594,6 +605,7 @@ async def _stream_chat_chunks(
|
||||
)
|
||||
|
||||
# Create the stream with proper types
|
||||
# Pass langfuse_prompt to link generation to prompt version in Langfuse
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
@@ -601,6 +613,7 @@ async def _stream_chat_chunks(
|
||||
tool_choice="auto",
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
langfuse_prompt=langfuse_prompt, # type: ignore[call-overload]
|
||||
)
|
||||
|
||||
# Variables to accumulate tool calls
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
CredentialsMetaInput,
|
||||
CredentialsType,
|
||||
GraphExecutionID,
|
||||
GraphMeta,
|
||||
LibraryAgentPreset,
|
||||
@@ -29,7 +36,11 @@ import {
|
||||
} from "@/components/__legacy__/ui/icons";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
|
||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
||||
import {
|
||||
findSavedCredentialByProviderAndType,
|
||||
findSavedUserCredentialByProviderAndType,
|
||||
} from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import {
|
||||
useToast,
|
||||
@@ -37,6 +48,7 @@ import {
|
||||
} from "@/components/molecules/Toast/use-toast";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { cn, isEmpty } from "@/lib/utils";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
@@ -90,6 +102,7 @@ export function AgentRunDraftView({
|
||||
const api = useBackendAPI();
|
||||
const { toast } = useToast();
|
||||
const toastOnFail = useToastOnFail();
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
@@ -128,6 +141,77 @@ export function AgentRunDraftView({
|
||||
() => graph.credentials_input_schema.properties,
|
||||
[graph],
|
||||
);
|
||||
const credentialFields = useMemo(
|
||||
function getCredentialFields() {
|
||||
return Object.entries(agentCredentialsInputFields);
|
||||
},
|
||||
[agentCredentialsInputFields],
|
||||
);
|
||||
const requiredCredentials = useMemo(
|
||||
function getRequiredCredentials() {
|
||||
return new Set(
|
||||
(graph.credentials_input_schema?.required as string[]) || [],
|
||||
);
|
||||
},
|
||||
[graph.credentials_input_schema?.required],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function initializeDefaultCredentials() {
|
||||
if (!allProviders) return;
|
||||
if (!graph.credentials_input_schema?.properties) return;
|
||||
if (requiredCredentials.size === 0) return;
|
||||
|
||||
setInputCredentials(function updateCredentials(currentCreds) {
|
||||
const next = { ...currentCreds };
|
||||
let didAdd = false;
|
||||
|
||||
for (const key of requiredCredentials) {
|
||||
if (next[key]) continue;
|
||||
const schema = graph.credentials_input_schema.properties[key];
|
||||
if (!schema) continue;
|
||||
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const credentialTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
|
||||
const userCredential = findSavedUserCredentialByProviderAndType(
|
||||
providerNames,
|
||||
credentialTypes,
|
||||
requiredScopes,
|
||||
allProviders,
|
||||
);
|
||||
|
||||
const savedCredential =
|
||||
userCredential ||
|
||||
findSavedCredentialByProviderAndType(
|
||||
providerNames,
|
||||
credentialTypes,
|
||||
requiredScopes,
|
||||
allProviders,
|
||||
);
|
||||
|
||||
if (!savedCredential) continue;
|
||||
|
||||
next[key] = {
|
||||
id: savedCredential.id,
|
||||
provider: savedCredential.provider,
|
||||
type: savedCredential.type as CredentialsType,
|
||||
title: savedCredential.title,
|
||||
};
|
||||
didAdd = true;
|
||||
}
|
||||
|
||||
if (!didAdd) return currentCreds;
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[
|
||||
allProviders,
|
||||
graph.credentials_input_schema?.properties,
|
||||
requiredCredentials,
|
||||
],
|
||||
);
|
||||
|
||||
const [allRequiredInputsAreSet, missingInputs] = useMemo(() => {
|
||||
const nonEmptyInputs = new Set(
|
||||
@@ -145,18 +229,35 @@ export function AgentRunDraftView({
|
||||
);
|
||||
return [isSuperset, difference];
|
||||
}, [agentInputSchema.required, inputValues]);
|
||||
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
|
||||
const availableCredentials = new Set(Object.keys(inputCredentials));
|
||||
const allCredentials = new Set(Object.keys(agentCredentialsInputFields));
|
||||
// Backwards-compatible implementation of isSupersetOf and difference
|
||||
const isSuperset = Array.from(allCredentials).every((item) =>
|
||||
availableCredentials.has(item),
|
||||
);
|
||||
const difference = Array.from(allCredentials).filter(
|
||||
(item) => !availableCredentials.has(item),
|
||||
);
|
||||
return [isSuperset, difference];
|
||||
}, [agentCredentialsInputFields, inputCredentials]);
|
||||
const [allCredentialsAreSet, missingCredentials] = useMemo(
|
||||
function getCredentialStatus() {
|
||||
const missing = Array.from(requiredCredentials).filter((key) => {
|
||||
const cred = inputCredentials[key];
|
||||
return !cred || !cred.id;
|
||||
});
|
||||
return [missing.length === 0, missing];
|
||||
},
|
||||
[requiredCredentials, inputCredentials],
|
||||
);
|
||||
function addChangedCredentials(prev: Set<keyof LibraryAgentPresetUpdatable>) {
|
||||
const next = new Set(prev);
|
||||
next.add("credentials");
|
||||
return next;
|
||||
}
|
||||
|
||||
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
|
||||
setInputCredentials(function updateInputCredentials(currentCreds) {
|
||||
const next = { ...currentCreds };
|
||||
if (value === undefined) {
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
next[key] = value;
|
||||
return next;
|
||||
});
|
||||
setChangedPresetAttributes(addChangedCredentials);
|
||||
}
|
||||
|
||||
const notifyMissingInputs = useCallback(
|
||||
(needPresetName: boolean = true) => {
|
||||
const allMissingFields = (
|
||||
@@ -649,35 +750,6 @@ export function AgentRunDraftView({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Credentials inputs */}
|
||||
{Object.entries(agentCredentialsInputFields).map(
|
||||
([key, inputSubSchema]) => (
|
||||
<CredentialsInput
|
||||
key={key}
|
||||
schema={{ ...inputSubSchema, discriminator: undefined }}
|
||||
selectedCredentials={
|
||||
inputCredentials[key] ?? inputSubSchema.default
|
||||
}
|
||||
onSelectCredentials={(value) => {
|
||||
setInputCredentials((obj) => {
|
||||
const newObj = { ...obj };
|
||||
if (value === undefined) {
|
||||
delete newObj[key];
|
||||
return newObj;
|
||||
}
|
||||
return {
|
||||
...obj,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
setChangedPresetAttributes((prev) =>
|
||||
prev.add("credentials"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Regular inputs */}
|
||||
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
|
||||
<RunAgentInputs
|
||||
@@ -695,6 +767,17 @@ export function AgentRunDraftView({
|
||||
data-testid={`agent-input-${key}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Credentials inputs */}
|
||||
{credentialFields.length > 0 && (
|
||||
<CredentialsGroupedView
|
||||
credentialFields={credentialFields}
|
||||
requiredCredentials={requiredCredentials}
|
||||
inputCredentials={inputCredentials}
|
||||
inputValues={inputValues}
|
||||
onCredentialChange={handleCredentialChange}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -4085,6 +4085,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/local-media/users/{user_id}/{media_type}/{filename}": {
|
||||
"get": {
|
||||
"tags": ["media", "media"],
|
||||
"summary": "Serve local media file",
|
||||
"description": "Serve a media file from local storage.\nOnly available when GCS is not configured.",
|
||||
"operationId": "getMediaServe local media file",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "User Id" }
|
||||
},
|
||||
{
|
||||
"name": "media_type",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Media Type" }
|
||||
},
|
||||
{
|
||||
"name": "filename",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Filename" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/oauth/app/{client_id}": {
|
||||
"get": {
|
||||
"tags": ["oauth"],
|
||||
@@ -10187,6 +10229,13 @@
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Context"
|
||||
},
|
||||
"tags": {
|
||||
"anyOf": [
|
||||
{ "items": { "type": "string" }, "type": "array" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { getSystemCredentials } from "../../helpers";
|
||||
import { filterSystemCredentials, getSystemCredentials } from "../../helpers";
|
||||
|
||||
export type CredentialField = [string, any];
|
||||
|
||||
@@ -208,3 +208,42 @@ export function findSavedCredentialByProviderAndType(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findSavedUserCredentialByProviderAndType(
|
||||
providerNames: string[],
|
||||
credentialTypes: string[],
|
||||
requiredScopes: string[] | undefined,
|
||||
allProviders: CredentialsProvidersContextType | null,
|
||||
): SavedCredential | undefined {
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders?.[providerName];
|
||||
if (!providerData) continue;
|
||||
|
||||
const userCredentials = filterSystemCredentials(
|
||||
providerData.savedCredentials ?? [],
|
||||
);
|
||||
|
||||
const matchingCredentials: SavedCredential[] = [];
|
||||
|
||||
for (const credential of userCredentials) {
|
||||
const typeMatches =
|
||||
credentialTypes.length === 0 ||
|
||||
credentialTypes.includes(credential.type);
|
||||
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
|
||||
|
||||
if (!typeMatches) continue;
|
||||
if (!scopesMatch) continue;
|
||||
|
||||
matchingCredentials.push(credential as SavedCredential);
|
||||
}
|
||||
|
||||
if (matchingCredentials.length === 1) {
|
||||
return matchingCredentials[0];
|
||||
}
|
||||
if (matchingCredentials.length > 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -98,24 +98,20 @@ export function useCredentialsInput({
|
||||
|
||||
// Auto-select the first available credential on initial mount
|
||||
// Once a user has made a selection, we don't override it
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
useEffect(
|
||||
function autoSelectCredential() {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
if (selectedCredential?.id) return;
|
||||
|
||||
// If already selected, don't auto-select
|
||||
if (selectedCredential?.id) return;
|
||||
const savedCreds = credentials.savedCredentials;
|
||||
if (savedCreds.length === 0) return;
|
||||
|
||||
// Only attempt auto-selection once
|
||||
if (hasAttemptedAutoSelect.current) return;
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
if (hasAttemptedAutoSelect.current) return;
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
|
||||
// If optional, don't auto-select (user can choose "None")
|
||||
if (isOptional) return;
|
||||
if (isOptional) return;
|
||||
|
||||
const savedCreds = credentials.savedCredentials;
|
||||
|
||||
// Auto-select the first credential if any are available
|
||||
if (savedCreds.length > 0) {
|
||||
const cred = savedCreds[0];
|
||||
onSelectCredential({
|
||||
id: cred.id,
|
||||
@@ -123,14 +119,15 @@ export function useCredentialsInput({
|
||||
provider: credentials.provider,
|
||||
title: (cred as any).title,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
credentials,
|
||||
selectedCredential?.id,
|
||||
readOnly,
|
||||
isOptional,
|
||||
onSelectCredential,
|
||||
]);
|
||||
},
|
||||
[
|
||||
credentials,
|
||||
selectedCredential?.id,
|
||||
readOnly,
|
||||
isOptional,
|
||||
onSelectCredential,
|
||||
],
|
||||
);
|
||||
|
||||
if (
|
||||
!credentials ||
|
||||
|
||||
@@ -106,9 +106,14 @@ export function getTimezoneDisplayName(timezone: string): string {
|
||||
const parts = timezone.split("/");
|
||||
const city = parts[parts.length - 1].replace(/_/g, " ");
|
||||
const abbr = getTimezoneAbbreviation(timezone);
|
||||
return abbr ? `${city} (${abbr})` : city;
|
||||
if (abbr && abbr !== timezone) {
|
||||
return `${city} (${abbr})`;
|
||||
}
|
||||
// If abbreviation is same as timezone or not found, show timezone with underscores replaced
|
||||
const timezoneDisplay = timezone.replace(/_/g, " ");
|
||||
return `${city} (${timezoneDisplay})`;
|
||||
} catch {
|
||||
return timezone;
|
||||
return timezone.replace(/_/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user