fix: changes to update pipecat version to 0.0.100 (#122)

* feat: add stt evals

* add smart turn as provider

* chore: remove deprecations

* chore: format files

* fix: remove deprecated UserIdleProcessor

* fix: remove deprecated TranscriptProcessor

* chore: update pipecat submodule

* feat: add evals visualisation

* fix: trigger llm generation on client connected and pipeline started

* chore: update pipecat

* chore: update pipecat submodule

* Add tests

* fix: slow loading of workflow page

* chore: update pipecat submodule

* Show version after release

* Fixes #99

* fix: provider check for websocket connection

* Fixes #107

* Fix #96

* chore: fix documentation

* fix: cloudonix campaign call error

---------

Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
Abhishek 2026-01-23 18:53:59 +05:30 committed by GitHub
parent a4367bd83b
commit 911c5ed416
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 16919 additions and 597 deletions

View file

@ -1,6 +1,6 @@
import { redirect } from "next/navigation";
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
import { getServerAccessToken,getServerAuthProvider, getServerUser } from "@/lib/auth/server";
import logger from '@/lib/logger';
import { getRedirectUrl } from "@/lib/utils";
@ -34,21 +34,18 @@ export default async function AfterSignInPage() {
try {
const accessToken = await getServerAccessToken();
if (accessToken) {
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
const activeWorkflows = workflows.filter(w => w.status === 'active');
logger.debug('[AfterSignInPage] Found workflows:', {
total: workflows.length,
active: activeWorkflows.length
total: countResponse.data?.total,
active: countResponse.data?.active
});
if (activeWorkflows.length > 0) {
if (countResponse.data && countResponse.data.active > 0) {
logger.debug('[AfterSignInPage] Redirecting to /workflow - user has workflows');
redirect('/workflow');
} else {

View file

@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { healthApiV1HealthGet } from "@/client/sdk.gen";
import type { HealthResponse } from "@/client/types.gen";
// Import version from package.json at build time
import packageJson from "../../../../../package.json";
export async function GET() {
const uiVersion = packageJson.version || "dev";
// Fetch backend version and config from health endpoint
let apiVersion = "unknown";
let backendApiEndpoint: string | null = null;
try {
const response = await healthApiV1HealthGet();
if (response.data) {
const data = response.data as HealthResponse;
apiVersion = data.version;
backendApiEndpoint = data.backend_api_endpoint;
}
} catch {
// Backend might not be reachable during build or in some deployments
apiVersion = "unavailable";
}
return NextResponse.json({
ui: uiVersion,
api: apiVersion,
backendApiEndpoint,
});
}

View file

@ -9,6 +9,7 @@ import AppLayout from "@/components/layout/AppLayout";
import PostHogIdentify from "@/components/PostHogIdentify";
import SpinLoader from "@/components/SpinLoader";
import { Toaster } from "@/components/ui/sonner";
import { AppConfigProvider } from "@/context/AppConfigContext";
import { OnboardingProvider } from "@/context/OnboardingContext";
import { UserConfigProvider } from "@/context/UserConfigContext";
import { AuthProvider } from "@/lib/auth";
@ -59,18 +60,20 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<AuthProvider>
<Suspense fallback={<SpinLoader />}>
<UserConfigProvider>
<OnboardingProvider>
<PostHogIdentify />
<AppLayout>
{children}
</AppLayout>
<Toaster />
<ChatwootWidget />
</OnboardingProvider>
</UserConfigProvider>
</Suspense>
<AppConfigProvider>
<Suspense fallback={<SpinLoader />}>
<UserConfigProvider>
<OnboardingProvider>
<PostHogIdentify />
<AppLayout>
{children}
</AppLayout>
<Toaster />
<ChatwootWidget />
</OnboardingProvider>
</UserConfigProvider>
</Suspense>
</AppConfigProvider>
</AuthProvider>
</body>
</html>

View file

@ -1,7 +1,7 @@
import { isNextRouterError } from "next/dist/client/components/is-next-router-error";
import { redirect } from "next/navigation";
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
import SignInClient from "@/components/SignInClient";
import { getServerAccessToken,getServerAuthProvider,getServerUser } from "@/lib/auth/server";
import logger from '@/lib/logger';
@ -21,21 +21,18 @@ export default async function Home() {
try {
const accessToken = await getServerAccessToken();
if (accessToken) {
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
const activeWorkflows = workflows.filter(w => w.status === 'active');
logger.debug('[HomePage] Found workflows for local provider:', {
total: workflows.length,
active: activeWorkflows.length
total: countResponse.data?.total,
active: countResponse.data?.active
});
if (activeWorkflows.length > 0) {
if (countResponse.data && countResponse.data.active > 0) {
logger.debug('[HomePage] Redirecting to /workflow - user has workflows');
redirect('/workflow');
} else {

View file

@ -326,14 +326,64 @@ export default function UsagePage() {
isDisabled={savingTimezone || userConfigLoading}
placeholder={userConfigLoading ? "Loading..." : "Select timezone"}
styles={{
control: (base) => ({
control: (base, state) => ({
...base,
minHeight: '36px',
fontSize: '14px',
backgroundColor: 'var(--background)',
borderColor: state.isFocused ? 'var(--ring)' : 'var(--border)',
boxShadow: state.isFocused ? '0 0 0 2px color-mix(in srgb, var(--ring) 20%, transparent)' : 'none',
'&:hover': {
borderColor: 'var(--border)',
},
}),
menu: (base) => ({
...base,
zIndex: 9999,
backgroundColor: 'var(--popover)',
border: '1px solid var(--border)',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
}),
menuList: (base) => ({
...base,
backgroundColor: 'var(--popover)',
padding: 0,
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected
? 'var(--accent)'
: state.isFocused
? 'var(--accent)'
: 'var(--popover)',
color: 'var(--foreground)',
cursor: 'pointer',
'&:active': {
backgroundColor: 'var(--accent)',
},
}),
singleValue: (base) => ({
...base,
color: 'var(--foreground)',
}),
input: (base) => ({
...base,
color: 'var(--foreground)',
}),
placeholder: (base) => ({
...base,
color: 'var(--muted-foreground)',
}),
indicatorSeparator: (base) => ({
...base,
backgroundColor: 'var(--border)',
}),
dropdownIndicator: (base) => ({
...base,
color: 'var(--muted-foreground)',
'&:hover': {
color: 'var(--foreground)',
},
}),
}}
/>

File diff suppressed because one or more lines are too long

View file

@ -524,6 +524,12 @@ export type HttpValidationError = {
detail?: Array<ValidationError>;
};
export type HealthResponse = {
status: string;
version: string;
backend_api_endpoint: string;
};
/**
* Configuration for HTTP API tools.
*/
@ -1042,6 +1048,15 @@ export type VonageConfigurationResponse = {
*/
export type WebhookCredentialType = 'none' | 'api_key' | 'bearer_token' | 'basic_auth' | 'custom_header';
/**
* Response for workflow count endpoint.
*/
export type WorkflowCountResponse = {
total: number;
active: number;
archived: number;
};
export type WorkflowError = {
kind: ItemKind;
id: string | null;
@ -1049,6 +1064,17 @@ export type WorkflowError = {
message: string;
};
/**
* Lightweight response for workflow listings (excludes large fields).
*/
export type WorkflowListResponse = {
id: number;
name: string;
status: string;
created_at: string;
total_runs: number;
};
export type WorkflowOption = {
id: number;
name: string;
@ -1391,6 +1417,7 @@ export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData = {
'x-twilio-signature'?: string | null;
'x-vobiz-signature'?: string | null;
'x-vobiz-timestamp'?: string | null;
'x-cx-apikey'?: string | null;
};
path: {
workflow_id: number;
@ -1655,6 +1682,39 @@ export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses =
export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse = CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses[keyof CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses];
export type GetWorkflowCountApiV1WorkflowCountGetData = {
body?: never;
headers?: {
authorization?: string | null;
'X-API-Key'?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/workflow/count';
};
export type GetWorkflowCountApiV1WorkflowCountGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetWorkflowCountApiV1WorkflowCountGetError = GetWorkflowCountApiV1WorkflowCountGetErrors[keyof GetWorkflowCountApiV1WorkflowCountGetErrors];
export type GetWorkflowCountApiV1WorkflowCountGetResponses = {
/**
* Successful Response
*/
200: WorkflowCountResponse;
};
export type GetWorkflowCountApiV1WorkflowCountGetResponse = GetWorkflowCountApiV1WorkflowCountGetResponses[keyof GetWorkflowCountApiV1WorkflowCountGetResponses];
export type GetWorkflowsApiV1WorkflowFetchGetData = {
body?: never;
headers?: {
@ -1688,7 +1748,7 @@ export type GetWorkflowsApiV1WorkflowFetchGetResponses = {
/**
* Successful Response
*/
200: Array<WorkflowResponse>;
200: Array<WorkflowListResponse>;
};
export type GetWorkflowsApiV1WorkflowFetchGetResponse = GetWorkflowsApiV1WorkflowFetchGetResponses[keyof GetWorkflowsApiV1WorkflowFetchGetResponses];
@ -4168,6 +4228,41 @@ export type InitiateCallApiV1PublicAgentUuidPostResponses = {
export type InitiateCallApiV1PublicAgentUuidPostResponse = InitiateCallApiV1PublicAgentUuidPostResponses[keyof InitiateCallApiV1PublicAgentUuidPostResponses];
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData = {
body?: never;
path: {
token: string;
artifact_type: 'recording' | 'transcript';
};
query?: {
/**
* Display inline in browser instead of download
*/
inline?: boolean;
};
url: '/api/v1/public/download/workflow/{token}/{artifact_type}';
};
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError = DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors[keyof DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors];
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData = {
body?: never;
headers?: {
@ -4500,9 +4595,11 @@ export type HealthApiV1HealthGetResponses = {
/**
* Successful Response
*/
200: unknown;
200: HealthResponse;
};
export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof HealthApiV1HealthGetResponses];
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};

View file

@ -22,6 +22,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
const [isOpen, setIsOpen] = useState(false);
const [mediaType, setMediaType] = useState<'audio' | 'transcript' | null>(null);
const [mediaSignedUrl, setMediaSignedUrl] = useState<string | null>(null);
const [transcriptContent, setTranscriptContent] = useState<string | null>(null);
const [selectedRunId, setSelectedRunId] = useState<number | null>(null);
const [mediaDownloadKey, setMediaDownloadKey] = useState<string | null>(null);
const [mediaLoading, setMediaLoading] = useState(false);
@ -47,6 +48,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
async (fileKey: string | null, runId: number) => {
if (!fileKey || !accessToken) return;
setMediaLoading(true);
setTranscriptContent(null);
const signed = await getSignedUrl(fileKey, accessToken, true);
if (signed) {
setMediaType('transcript');
@ -54,6 +56,14 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
setMediaDownloadKey(fileKey);
setSelectedRunId(runId);
setIsOpen(true);
// Fetch transcript content with proper UTF-8 encoding
try {
const response = await fetch(signed);
const text = await response.text();
setTranscriptContent(text);
} catch (error) {
console.error('Error fetching transcript:', error);
}
}
setMediaLoading(false);
},
@ -84,12 +94,10 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
<audio src={mediaSignedUrl} controls autoPlay className="w-full mt-4" />
)}
{!mediaLoading && mediaType === 'transcript' && mediaSignedUrl && (
<iframe
src={mediaSignedUrl}
title="Transcript"
className="w-full h-[60vh] border rounded-md mt-4"
/>
{!mediaLoading && mediaType === 'transcript' && transcriptContent && (
<pre className="w-full h-[60vh] overflow-auto border rounded-md mt-4 p-4 bg-muted text-sm whitespace-pre-wrap font-mono">
{transcriptContent}
</pre>
)}
<DialogFooter className="pt-4">

View file

@ -321,9 +321,20 @@ export default function ServiceConfiguration() {
if (!providerSchema) return [];
// Find all config fields (not provider, not api_key)
return Object.keys(providerSchema.properties).filter(
const fields = Object.keys(providerSchema.properties).filter(
field => field !== "provider" && field !== "api_key"
);
// For Deepgram STT, hide language field when flux-general-en model is selected
// Flux model is English-only and doesn't support language selection
if (service === "stt" && currentProvider === "deepgram") {
const currentModel = watch("stt_model") as string;
if (currentModel === "flux-general-en") {
return fields.filter(field => field !== "language");
}
}
return fields;
};
const renderServiceFields = (service: ServiceSegment) => {

View file

@ -35,7 +35,7 @@ interface EndCallNodeProps extends NodeProps {
}
export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
const { open, setOpen, handleSaveNodeData } = useNodeHandlers({
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({
id,
additionalData: { is_end: true }
});
@ -122,9 +122,14 @@ export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
</NodeContent>
<NodeToolbar isVisible={selected} position={Position.Right}>
<Button onClick={() => setOpen(true)} variant="outline" size="icon">
<Edit />
</Button>
<div className="flex flex-col gap-1">
<Button onClick={() => setOpen(true)} variant="outline" size="icon">
<Edit />
</Button>
<Button onClick={handleDeleteNode} variant="outline" size="icon">
<Trash2Icon />
</Button>
</div>
</NodeToolbar>
<NodeEditDialog

View file

@ -8,6 +8,7 @@ import { FlowNodeData } from "@/components/flow/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAppConfig } from "@/context/AppConfigContext";
import { NodeContent } from "./common/NodeContent";
import { NodeEditDialog } from "./common/NodeEditDialog";
@ -26,6 +27,7 @@ interface TriggerNodeProps extends NodeProps {
export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id });
const { saveWorkflow } = useWorkflow();
const { config } = useAppConfig();
// Form state
const [name, setName] = useState(data.name || "API Trigger");
@ -33,8 +35,9 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
// Generate trigger_path if not present (should be done on node creation)
const [triggerPath] = useState(() => data.trigger_path ?? crypto.randomUUID());
// Get backend URL from environment
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
// Get backend URL from app config (fetched from backend health endpoint)
// Falls back to env variable, then to localhost for local development
const backendUrl = config?.backendApiEndpoint || process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
const endpoint = `${backendUrl}/api/v1/public/agent/${triggerPath}`;
// Copy state for button feedback

View file

@ -46,6 +46,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppConfig } from "@/context/AppConfigContext";
import { useAuth } from "@/lib/auth";
import { cn } from "@/lib/utils";
@ -66,10 +67,14 @@ export function AppSidebar() {
const router = useRouter();
const { state } = useSidebar();
const { provider, getSelectedTeam } = useAuth();
const { config } = useAppConfig();
// Get selected team for Stack auth (cast to Team type from Stack)
const selectedTeam = provider === "stack" && getSelectedTeam ? getSelectedTeam() as Team | null : null;
// Version info from app config context
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
const isActive = (path: string) => {
return pathname.startsWith(path);
};
@ -207,6 +212,11 @@ export function AppSidebar() {
className="flex items-center gap-2 px-2 text-xl font-bold"
>
Dograh
{versionInfo && (
<span className="text-xs font-normal text-muted-foreground">
v{versionInfo.ui}
</span>
)}
</Link>
)}
{/* Toggle button - center it when collapsed */}
@ -445,6 +455,7 @@ export function AppSidebar() {
/>
)}
</div>
</div>
</SidebarFooter>
<SidebarRail />

View file

@ -0,0 +1,58 @@
'use client';
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
interface AppConfig {
uiVersion: string;
apiVersion: string;
backendApiEndpoint: string | null;
}
interface AppConfigContextType {
config: AppConfig | null;
loading: boolean;
}
const defaultConfig: AppConfig = {
uiVersion: 'dev',
apiVersion: 'unknown',
backendApiEndpoint: null,
};
const AppConfigContext = createContext<AppConfigContextType>({
config: null,
loading: true,
});
export function AppConfigProvider({ children }: { children: ReactNode }) {
const [config, setConfig] = useState<AppConfig | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/config/version')
.then((res) => res.json())
.then((data) => {
setConfig({
uiVersion: data.ui || 'dev',
apiVersion: data.api || 'unknown',
backendApiEndpoint: data.backendApiEndpoint || null,
});
})
.catch(() => {
setConfig(defaultConfig);
})
.finally(() => {
setLoading(false);
});
}, []);
return (
<AppConfigContext.Provider value={{ config, loading }}>
{children}
</AppConfigContext.Provider>
);
}
export function useAppConfig() {
return useContext(AppConfigContext);
}

View file

@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { getAuthUserApiV1UserAuthUserGet } from "@/client/sdk.gen";
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
import { impersonateApiV1SuperuserImpersonatePost } from "@/client/sdk.gen";
export function cn(...inputs: ClassValue[]) {
@ -73,21 +73,18 @@ export async function getRedirectUrl(token: string, permissions: { id: string }[
// Check if user has any workflows
try {
console.log('[getRedirectUrl] Checking for existing workflows...');
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
headers: {
Authorization: `Bearer ${token}`,
},
});
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
const activeWorkflows = workflows.filter(w => w.status === 'active');
console.log('[getRedirectUrl] Found workflows:', {
total: workflows.length,
active: activeWorkflows.length
total: countResponse.data?.total,
active: countResponse.data?.active
});
if (activeWorkflows.length > 0) {
if (countResponse.data && countResponse.data.active > 0) {
console.log('[getRedirectUrl] User has workflows, redirecting to /workflow');
return "/workflow";
} else {