Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6906734
feat(packages/kb-management): scaffold KB management package mounted …
jabbadizzleCode Jul 17, 2026
546d48c
feat(kb): knowledge base CRUD across graphql and kb-management package
jabbadizzleCode Jul 17, 2026
651a9bb
feat(kb): blob upload and web-resource registration for KB resources
jabbadizzleCode Jul 17, 2026
fbd4dcd
feat(kb): hatchet ingestion dispatch task and ingest trigger mutation
jabbadizzleCode Jul 17, 2026
fec1f4a
feat(kb): signed ingestion webhook receiver updating resource status
jabbadizzleCode Jul 17, 2026
2174fae
enhance(kb): status badges, live polling, and responsive polish
jabbadizzleCode Jul 17, 2026
4eefc28
chore(kb): final checks and dev docs for KB POC
jabbadizzleCode Jul 17, 2026
5df4394
fix(kb): protect claimed blobs during upload confirmation
jabbadizzleCode Jul 17, 2026
cac0fa1
docs(kb): record finish-gate review outcomes
jabbadizzleCode Jul 17, 2026
f0a109b
docs(kb): design external Hatchet ingestion bridge
jabbadizzleCode Jul 20, 2026
226cb1c
docs(kb): plan external Hatchet ingestion bridge
jabbadizzleCode Jul 20, 2026
d74ef2b
feat(kb): track latest external ingestion attempt
jabbadizzleCode Jul 20, 2026
b4e84e5
feat(kb): correlate signed ingestion callbacks
jabbadizzleCode Jul 20, 2026
cacba15
feat(kb): add correlated speed-aware ingestion attempts
jabbadizzleCode Jul 20, 2026
31b588d
feat(kb): dispatch selected resources to external Hatchet
jabbadizzleCode Jul 20, 2026
b93d0eb
feat(kb): monitor external ingestion runs with one cron
jabbadizzleCode Jul 20, 2026
64b1b9f
feat(kb): select ingestion speed per resource
jabbadizzleCode Jul 20, 2026
9007e84
chore(kb): configure external Hatchet ingestion bridge
jabbadizzleCode Jul 20, 2026
c722f52
fix(kb): close external ingestion review findings
jabbadizzleCode Jul 20, 2026
b291d71
fix(kb): bound serialized blob deletion
jabbadizzleCode Jul 20, 2026
b4a9989
docs(kb): record external ingestion bridge verification
jabbadizzleCode Jul 20, 2026
49481a3
docs(kg): add chatbot graph visualization design
jabbadizzleCode Jul 20, 2026
b9bcbbd
docs(kg): add visualization implementation plan
jabbadizzleCode Jul 20, 2026
c91d35e
feat(kg): add shared graph contracts
jabbadizzleCode Jul 20, 2026
70b94eb
feat(kg): persist chatbot graph lifecycle
jabbadizzleCode Jul 20, 2026
e161740
feat(kg): manage chatbot graph resources
jabbadizzleCode Jul 20, 2026
88b5e07
feat(kg): add bounded FalkorDB reader
jabbadizzleCode Jul 20, 2026
009e6b7
feat(kg): dispatch chatbot graph builds
jabbadizzleCode Jul 20, 2026
4a72019
feat(kg): monitor chatbot graph builds
jabbadizzleCode Jul 20, 2026
1ea9328
feat(kg): add interactive graph viewer
jabbadizzleCode Jul 20, 2026
f67ee13
feat(kg): expose lecturer graph preview
jabbadizzleCode Jul 20, 2026
9f61905
feat(chat): serve authorized knowledge graphs
jabbadizzleCode Jul 20, 2026
82fd2fe
chore(kg): wire FalkorDB configuration
jabbadizzleCode Jul 20, 2026
6198749
feat(manage): configure chatbot knowledge graphs
jabbadizzleCode Jul 20, 2026
7b4489c
feat(chat): add knowledge graph workspace
jabbadizzleCode Jul 20, 2026
101709d
fix(kg): validate recovered Hatchet runs
jabbadizzleCode Jul 20, 2026
ea9681a
fix(chat): stabilize knowledge graph viewer
jabbadizzleCode Jul 20, 2026
576969e
refactor(kg): extract cytoscape presentation
jabbadizzleCode Jul 20, 2026
edf3a5a
docs(kg): add browser verification screenshots
jabbadizzleCode Jul 20, 2026
f8e4ffc
docs: design Hatchet worker dev startup ordering
jabbadizzleCode Jul 21, 2026
b525942
fix(hatchet): stabilize worker startup in development
jabbadizzleCode Jul 21, 2026
98a0ad5
fix(chat): restore knowledge graph runtime
jabbadizzleCode Jul 21, 2026
f2145bc
fix(ci): build knowledge graph before graphql
jabbadizzleCode Jul 21, 2026
6d90ca8
docs(kg): design model selection
jabbadizzleCode Jul 21, 2026
99645a2
feat(kg): select ingestion models
jabbadizzleCode Jul 21, 2026
9b5fc7a
feat(hatchet): update KB ingestion model IDs and add Swiss Foundry mo…
jabbadizzleCode Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/check-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ jobs:
pnpm run build
cd ../util
pnpm run build
cd ../knowledge-graph
pnpm run build
cd ../graphql
pnpm run build
cd ../hatchet
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test-graphql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ jobs:
pnpm run build
cd ../util
pnpm run build
cd ../knowledge-graph
pnpm run build
cd ../graphql
pnpm run build
cd ../hatchet
Expand Down
1 change: 1 addition & 0 deletions apps/backend-docker/.env.cypress
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ HATCHET_CLIENT_TOKEN=__HATCHET_CLIENT_TOKEN__
HATCHET_CLIENT_HOST_PORT=localhost:7077
HATCHET_CLIENT_TLS_STRATEGY=none
HATCHET_LOG_LEVEL=DEBUG
KB_WEBHOOK_SECRET="dev-kb-webhook-secret"

# Canonical app origins (prefer these; issuers fall back to them)
APP_ORIGIN_AUTH="http://127.0.0.1:3010"
Expand Down
9 changes: 9 additions & 0 deletions apps/backend-docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ HATCHET_CLIENT_TOKEN=__HATCHET_CLIENT_TOKEN__
HATCHET_CLIENT_HOST_PORT=localhost:7077
HATCHET_CLIENT_TLS_STRATEGY=none
HATCHET_LOG_LEVEL=DEBUG
KB_WEBHOOK_SECRET="dev-kb-webhook-secret"

# Canonical app origins (prefer these; issuers fall back to them)
APP_ORIGIN_AUTH="http://127.0.0.1:3010"
Expand All @@ -23,3 +24,11 @@ APP_ORIGIN_MANAGE="http://127.0.0.1:3002"
APP_ORIGIN_CONTROL="http://127.0.0.1:3003"
APP_ORIGIN_ASSESSMENT_API="http://127.0.0.1:3000"
APP_ORIGIN_ASSESSMENT_PWA="http://127.0.0.1:3001"

# Example-only local FalkorDB settings. Replace them through your secret manager.
KB_FALKORDB_HOST=localhost
KB_FALKORDB_PORT=6379
KB_FALKORDB_USERNAME=example-user
KB_FALKORDB_PASSWORD=example-password
KB_FALKORDB_TLS=false
KB_FALKORDB_QUERY_TIMEOUT_MS=5000
29 changes: 28 additions & 1 deletion apps/backend-docker/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { EnvelopArmor } from '@escape.tech/graphql-armor'
import { useCSRFPrevention } from '@graphql-yoga/plugin-csrf-prevention'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
// import { useResponseCache } from '@graphql-yoga/plugin-response-cache'
import { enhanceContext, schema } from '@klicker-uzh/graphql'
import {
enhanceContext,
handleKBIngestionWebhook,
schema,
} from '@klicker-uzh/graphql'
import { verifyJWT } from '@klicker-uzh/util'
import cookieParser from 'cookie-parser'
import cors from 'cors'
Expand Down Expand Up @@ -191,6 +195,29 @@ function prepareApp({
res.send('OK')
})

app.post(
'/api/webhooks/kb-ingestion',
express.raw({ type: 'application/json', limit: '1mb' }),
async (req, res) => {
try {
if (!Buffer.isBuffer(req.body)) {
res.status(400).json({ error: 'Invalid request' })
return
}

const result = await handleKBIngestionWebhook({
prisma,
rawBody: req.body,
headers: req.headers,
})
res.status(result.statusCode).json(result.body)
} catch (error) {
console.error('KB ingestion webhook failed', error)
res.status(500).json({ error: 'Internal server error' })
}
}
)

app.use('/api/graphql', yogaApp as any)

return { app, yogaApp }
Expand Down
8 changes: 8 additions & 0 deletions apps/chat/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ CHAT_OPENAI_STORE_RESPONSES=false

# MCP Key
MCP_KEY=sk-XXX

# Example-only local FalkorDB settings. Replace them through your secret manager.
KB_FALKORDB_HOST=localhost
KB_FALKORDB_PORT=6379
KB_FALKORDB_USERNAME=example-user
KB_FALKORDB_PASSWORD=example-password
KB_FALKORDB_TLS=false
KB_FALKORDB_QUERY_TIMEOUT_MS=5000
28 changes: 18 additions & 10 deletions apps/chat/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { getNextBaseConfig } from '@klicker-uzh/next-config'
import { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
import { fileURLToPath } from 'node:url'

const withNextIntl = createNextIntlPlugin('./src/types/i18n.ts')
const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url))
const baseConfig = getNextBaseConfig({
BLOB_STORAGE_ACCOUNT_URL: process.env.BLOB_STORAGE_ACCOUNT_URL ?? '',
NODE_ENV: process.env.NODE_ENV as string,
NEXT_PUBLIC_ENV: process.env.NEXT_PUBLIC_ENV as string,
})

// @ts-expect-error
const nextConfig: NextConfig = {
...getNextBaseConfig({
BLOB_STORAGE_ACCOUNT_URL: process.env.BLOB_STORAGE_ACCOUNT_URL ?? '',
NODE_ENV: process.env.NODE_ENV as string,
NEXT_PUBLIC_ENV: process.env.NEXT_PUBLIC_ENV as string,
}),
...baseConfig,
outputFileTracingRoot: repositoryRoot,
turbopack: {
root: repositoryRoot,
},
serverExternalPackages: [
'@klicker-uzh/knowledge-graph',
'@js-temporal/polyfill',
'falkordb',
'jsbi',
],
webpack: (config, { isServer }) => {
// Call the base config webpack function if it exists
const baseConfig = getNextBaseConfig({
BLOB_STORAGE_ACCOUNT_URL: process.env.BLOB_STORAGE_ACCOUNT_URL ?? '',
NODE_ENV: process.env.NODE_ENV as string,
NEXT_PUBLIC_ENV: process.env.NEXT_PUBLIC_ENV as string,
})
if (baseConfig.webpack) {
config = baseConfig.webpack(config, { isServer } as any)
}
Expand Down
1 change: 1 addition & 0 deletions apps/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@fortawesome/react-fontawesome": "0.2.2",
"@klicker-uzh/graphql": "workspace:*",
"@klicker-uzh/i18n": "workspace:*",
"@klicker-uzh/knowledge-graph": "workspace:*",
"@klicker-uzh/markdown": "workspace:*",
"@klicker-uzh/next-config": "workspace:*",
"@klicker-uzh/prisma": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions apps/chat/src/app/[chatbotId]/graph/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function KnowledgeGraphPage() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { withChatbotAuth } from '@/src/lib/server/apiGuards'
import {
type ChatbotKnowledgeGraphReadRequest,
isKnowledgeGraphNotPublishedError,
readPublishedChatbotKnowledgeGraph,
} from '@/src/lib/server/knowledgeGraph'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

export const runtime = 'nodejs'

const operationSchema = z.enum(['overview', 'search', 'neighbors'])
const searchQuerySchema = z.string().trim().min(1).max(100)
const nodeIdSchema = z.string().regex(/^\d+$/)

function invalidRequestResponse() {
return NextResponse.json(
{
code: 'INVALID_KNOWLEDGE_GRAPH_REQUEST',
error: 'Invalid knowledge graph request',
},
{ status: 400 }
)
}

function parseReadRequest(
req: NextRequest
): ChatbotKnowledgeGraphReadRequest | null {
const operation = operationSchema.safeParse(
req.nextUrl.searchParams.get('operation')
)
if (!operation.success) {
return null
}

if (operation.data === 'search') {
const query = searchQuerySchema.safeParse(req.nextUrl.searchParams.get('q'))
return query.success ? { operation: 'search', query: query.data } : null
}

if (operation.data === 'neighbors') {
const nodeId = nodeIdSchema.safeParse(
req.nextUrl.searchParams.get('nodeId')
)
return nodeId.success
? { operation: 'neighbors', nodeId: nodeId.data }
: null
}

return { operation: 'overview' }
}

export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ chatbotId: string }> }
) {
const { chatbotId } = await params
const authResult = await withChatbotAuth(req, chatbotId)
if ('response' in authResult) {
return authResult.response
}

const readRequest = parseReadRequest(req)
if (readRequest === null) {
return invalidRequestResponse()
}

try {
const response = await readPublishedChatbotKnowledgeGraph(
chatbotId,
readRequest
)
return NextResponse.json(response)
} catch (error) {
if (isKnowledgeGraphNotPublishedError(error)) {
return NextResponse.json(
{
code: 'KNOWLEDGE_GRAPH_NOT_PUBLISHED',
error: 'Knowledge graph is not published',
publicationStatus: error.code,
},
{ status: 409 }
)
}

console.error('Participant knowledge graph read failed', {
chatbotId,
operation: readRequest.operation,
})
return NextResponse.json(
{
code: 'KNOWLEDGE_GRAPH_TEMPORARILY_UNAVAILABLE',
error: 'Knowledge graph is temporarily unavailable',
},
{ status: 503 }
)
}
}
1 change: 1 addition & 0 deletions apps/chat/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@plugin "@tailwindcss/typography";

@source "../../node_modules/@uzh-bf/design-system/src";
@source "../../node_modules/@klicker-uzh/shared-components/src";

@custom-variant dark (&:is(.dark *));

Expand Down
4 changes: 4 additions & 0 deletions apps/chat/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import * as React from 'react'
import { ChatGraphModeSwitch } from './knowledge-graph/ChatGraphModeSwitch'
import { SettingsPanel } from './settings-panel'
import { ThreadList } from './thread-list'
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'
Expand Down Expand Up @@ -84,6 +85,9 @@ export function AppSidebar({
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<div className="border-b border-[#E9E9E9] px-2 pb-2">
<ChatGraphModeSwitch chatbotId={chatbotId} compact />
</div>
<ThreadList />
</SidebarContent>

Expand Down
39 changes: 34 additions & 5 deletions apps/chat/src/components/assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@uzh-bf/design-system'
import { Loader2, Plus } from 'lucide-react'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import { useParams, usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import { RuntimeProvider } from '../app/RuntimeProvider'
Expand All @@ -21,6 +21,8 @@ import { AppSidebar } from './app-sidebar'
import { ChatUiProvider, useChatUi } from './chat-ui-context'
import { DisclaimerModal } from './disclaimer-modal'
import { EmbeddedSettings } from './embedded-settings'
import { ChatGraphModeSwitch } from './knowledge-graph/ChatGraphModeSwitch'
import { ChatKnowledgeGraphWorkspace } from './knowledge-graph/ChatKnowledgeGraphWorkspace'
import { Thread } from './thread'
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'

Expand Down Expand Up @@ -306,9 +308,11 @@ export const Assistant = ({

function SidebarMain({
chatbot,
graphMode,
showFooter,
}: {
chatbot: { id: string; name: string; avatar?: string }
graphMode: boolean
showFooter: boolean
}) {
const { open } = useSidebar()
Expand All @@ -327,7 +331,7 @@ function SidebarMain({
}

return (
<SidebarInset>
<SidebarInset className="min-w-0">
<div
className={twMerge(
'flex shrink-0 items-center gap-2 border-b bg-gray-50 px-2 py-1.5',
Expand All @@ -350,14 +354,26 @@ function SidebarMain({
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
</div>
<div
className={twMerge(
'shrink-0 border-b border-[#E9E9E9] bg-white p-2',
open && 'md:hidden'
)}
>
<ChatGraphModeSwitch chatbotId={chatbot.id} />
</div>
<div className="flex min-h-0 flex-1 flex-col">
<div className="relative flex min-h-0 flex-1 flex-col">
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white">
<Loader2 className="text-muted-foreground size-6 animate-spin" />
</div>
)}
<Thread chatbotAvatar={chatbot.avatar ?? ''} />
{graphMode ? (
<ChatKnowledgeGraphWorkspace chatbotId={chatbot.id} />
) : (
<Thread chatbotAvatar={chatbot.avatar ?? ''} />
)}
</div>
{showFooter && <Footer />}
</div>
Expand All @@ -371,12 +387,18 @@ function AssistantLayout({
chatbot: { id: string; name: string; avatar?: string }
}) {
const { showSidebar, showFooter } = useChatUi()
const pathname = usePathname()
const graphMode = pathname === `/${chatbot.id}/graph`

if (showSidebar) {
return (
<SidebarProvider className="h-dvh overflow-hidden">
<AppSidebar chatbotName={chatbot.name} />
<SidebarMain chatbot={chatbot} showFooter={showFooter} />
<SidebarMain
chatbot={chatbot}
graphMode={graphMode}
showFooter={showFooter}
/>
</SidebarProvider>
)
}
Expand All @@ -389,8 +411,15 @@ function AssistantLayout({
</div>
<EmbeddedSettings />
</div>
<div className="shrink-0 border-b border-[#E9E9E9] bg-white p-2">
<ChatGraphModeSwitch chatbotId={chatbot.id} />
</div>
<div className="flex min-h-0 flex-1 flex-col">
<Thread chatbotAvatar={chatbot.avatar ?? ''} />
{graphMode ? (
<ChatKnowledgeGraphWorkspace chatbotId={chatbot.id} />
) : (
<Thread chatbotAvatar={chatbot.avatar ?? ''} />
)}
{showFooter && <Footer />}
</div>
</div>
Expand Down
Loading
Loading