diff --git a/README.md b/README.md index 89474d3..795aa41 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,6 @@ Want to know more about its architecture and how it works? You can read it [here - **All Mode:** Searches the entire web to find the best results. - **Local Research Mode:** Research and interact with local files with citations. - **Chat Mode:** Have a truly creative conversation without web search. - - **Academic Search Mode:** Finds articles and papers, ideal for academic research. - - **YouTube Search Mode:** Finds YouTube videos based on the search query. - - **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha. - - **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query. - **Current Information:** Some search tools might give you outdated info because they use data from crawling bots and convert them into embeddings and store them in a index. Unlike them, Perplexica uses SearxNG, a metasearch engine to get the results and rerank and get the most relevant source out of it, ensuring you always get the latest information without the overhead of daily data updates. - **API**: Integrate Perplexica into your existing applications and make use of its capibilities. @@ -178,22 +174,6 @@ When running Perplexica behind a reverse proxy (like Nginx, Apache, or Traefik), This ensures that OpenSearch descriptions, browser integrations, and all URLs work properly when accessing Perplexica through your reverse proxy. -## One-Click Deployment - -[](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica) -[](https://repocloud.io/details/?app_id=267) -[](https://template.run.claw.cloud/?referralCode=U11MRQ8U9RM4&openapp=system-fastdeploy%3FtemplateName%3Dperplexica) - -## Upcoming Features - -- [x] Add settings page -- [x] Adding support for local LLMs -- [x] History Saving features -- [x] Introducing various Focus Modes -- [x] Adding API support -- [x] Adding Discover -- [ ] Finalizing Copilot Mode - ## Fork Improvements This fork adds several enhancements to the original Perplexica project: @@ -250,6 +230,9 @@ This fork adds several enhancements to the original Perplexica project: - Real-time preview system to test widget output before saving - Automatic refresh of stale widgets when navigating to dashboard +- ✅ **Observability**: Built-in support for tracing and monitoring LLM calls using Langfuse or LangSmith. + - See [Tracing LLM Calls in Perplexica](docs/installation/TRACING.md) for more details. + ### Bug Fixes - ✅ Improved history rewriting diff --git a/docs/installation/TRACING.md b/docs/installation/TRACING.md new file mode 100644 index 0000000..21a9271 --- /dev/null +++ b/docs/installation/TRACING.md @@ -0,0 +1,42 @@ + +# Tracing LLM Calls in Perplexica + +Perplexica supports tracing all LangChain and LangGraph LLM calls for debugging, analytics, and prompt transparency. You can use either Langfuse (self-hosted, private, or cloud) or LangSmith (cloud, by LangChain) for tracing. + +## Langfuse Tracing (Recommended for Private/Self-Hosted) + +Langfuse is an open-source, self-hostable observability platform for LLM applications. It allows you to trace prompts, completions, and tool calls **privately**—no data leaves your infrastructure if you self-host. + +### Setup + +1. **Deploy Langfuse** + - See: [Langfuse Self-Hosting Guide](https://langfuse.com/docs/self-hosting) + - You can also use the Langfuse Cloud if you prefer. + +2. **Configure Environment Variables** + - Add the following to your environment variables in docker-compose or your deployment environment: + + ```env + LANGFUSE_PUBLIC_KEY=your-public-key + LANGFUSE_SECRET_KEY=your-secret-key + LANGFUSE_BASE_URL=https://your-langfuse-instance.com + ``` + - These are required for the tracing integration to work. If not set, tracing is disabled gracefully. + +3. **Run Perplexica** + - All LLM and agent calls will be traced automatically. You can view traces in your Langfuse dashboard. + +## LangSmith Tracing (Cloud by LangChain) + +Perplexica also supports tracing via [LangSmith](https://smith.langchain.com/), the official observability platform by LangChain. + +- To enable LangSmith, follow the official guide: [LangSmith Observability Docs](https://docs.smith.langchain.com/observability) +- Set the required environment variables as described in their documentation. + +**LangSmith is a managed cloud service.** + +--- + +For more details on tracing, see the respective documentation: +- [Langfuse Documentation](https://langfuse.com/docs) +- [LangSmith Observability](https://docs.smith.langchain.com/observability) diff --git a/package-lock.json b/package-lock.json index b43f393..021cd1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "jsdom": "^26.1.0", "jspdf": "^3.0.1", "langchain": "^0.3.26", + "langfuse-langchain": "^3.38.4", "lucide-react": "^0.525.0", "luxon": "^3.7.1", "mammoth": "^1.9.1", @@ -8855,6 +8856,46 @@ } } }, + "node_modules/langfuse": { + "version": "3.38.4", + "resolved": "https://registry.npmjs.org/langfuse/-/langfuse-3.38.4.tgz", + "integrity": "sha512-2UqMeHLl3DGNX1Nh/cO4jGhk7TzDJ6gjQLlyS9rwFCKVO81xot6b58yeTsTB5YrWupWsOxQtMNoQYIQGOUlH9Q==", + "license": "MIT", + "dependencies": { + "langfuse-core": "^3.38.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/langfuse-core": { + "version": "3.38.4", + "resolved": "https://registry.npmjs.org/langfuse-core/-/langfuse-core-3.38.4.tgz", + "integrity": "sha512-onTAqcEGhoXuBgqDFXe2t+bt9Vi+5YChRgdz3voM49JKoHwtVZQiUdqTfjSivGR75eSbYoiaIL8IRoio+jaqwg==", + "license": "MIT", + "dependencies": { + "mustache": "^4.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/langfuse-langchain": { + "version": "3.38.4", + "resolved": "https://registry.npmjs.org/langfuse-langchain/-/langfuse-langchain-3.38.4.tgz", + "integrity": "sha512-7HJqouMrVOP9MFdu33M4G4uBFyQAIh/DqGYALfs41xqm7t99eZxKcTvt4rYZy67iQAhd58TG3q8+9haGzuLbOA==", + "license": "MIT", + "dependencies": { + "langfuse": "^3.38.4", + "langfuse-core": "^3.38.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "langchain": ">=0.0.157 <0.4.0" + } + }, "node_modules/langsmith": { "version": "0.3.55", "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.55.tgz", diff --git a/package.json b/package.json index 6418533..a5149a2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "jsdom": "^26.1.0", "jspdf": "^3.0.1", "langchain": "^0.3.26", + "langfuse-langchain": "^3.38.4", "lucide-react": "^0.525.0", "luxon": "^3.7.1", "mammoth": "^1.9.1", diff --git a/src/app/api/dashboard/process-widget/route.ts b/src/app/api/dashboard/process-widget/route.ts index 39cb86c..2ef97fc 100644 --- a/src/app/api/dashboard/process-widget/route.ts +++ b/src/app/api/dashboard/process-widget/route.ts @@ -16,6 +16,7 @@ import { allTools } from '@/lib/tools'; import { Source } from '@/lib/types/widget'; import { WidgetProcessRequest } from '@/lib/types/api'; import axios from 'axios'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; // Helper function to fetch content from a single source async function fetchSourceContent( @@ -149,6 +150,7 @@ async function processWithLLM( }, { recursionLimit: 15, // Limit recursion depth to prevent infinite loops + ...getLangfuseCallbacks(), }, ); diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts index 6e36586..930c6b9 100644 --- a/src/app/api/uploads/route.ts +++ b/src/app/api/uploads/route.ts @@ -17,6 +17,7 @@ import { ChatOpenAI } from '@langchain/openai'; import { ChatOllama } from '@langchain/ollama'; import { z } from 'zod'; import { withStructuredOutput } from '@/lib/utils/structuredOutput'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; interface FileRes { fileName: string; @@ -71,7 +72,9 @@ Generate topics that describe what this document is about, its domain, and key s name: 'generate_topics', }); - const result = await structuredLlm.invoke(prompt); + const result = await structuredLlm.invoke(prompt, { + ...getLangfuseCallbacks(), + }); console.log('Generated topics:', result.topics); // Filename is included for context return filename + ', ' + result.topics.join(', '); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 58ba83e..93549e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - +
{ const imageSearchChain = createImageSearchChain(llm, systemInstructions); - return imageSearchChain.invoke(input); + return imageSearchChain.invoke(input, { ...getLangfuseCallbacks() }); }; export default handleImageSearch; diff --git a/src/lib/chains/suggestionGeneratorAgent.ts b/src/lib/chains/suggestionGeneratorAgent.ts index ed8238b..82a305c 100644 --- a/src/lib/chains/suggestionGeneratorAgent.ts +++ b/src/lib/chains/suggestionGeneratorAgent.ts @@ -5,6 +5,7 @@ import formatChatHistoryAsString from '../utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ChatOpenAI } from '@langchain/openai'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; const suggestionGeneratorPrompt = ` You are an AI suggestion generator for an AI powered search engine. @@ -74,7 +75,9 @@ const generateSuggestions = ( llm, systemInstructions, ); - return suggestionGeneratorChain.invoke(input); + return suggestionGeneratorChain.invoke(input, { + ...getLangfuseCallbacks(), + }); }; export default generateSuggestions; diff --git a/src/lib/chains/videoSearchAgent.ts b/src/lib/chains/videoSearchAgent.ts index 7a715ee..bc61bac 100644 --- a/src/lib/chains/videoSearchAgent.ts +++ b/src/lib/chains/videoSearchAgent.ts @@ -10,6 +10,7 @@ import LineOutputParser from '../outputParsers/lineOutputParser'; import { searchSearxng } from '../searxng'; import { formatDateForLLM } from '../utils'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; const VideoSearchChainPrompt = ` # Instructions @@ -147,7 +148,7 @@ const handleVideoSearch = ( systemInstructions?: string, ) => { const VideoSearchChain = createVideoSearchChain(llm, systemInstructions); - return VideoSearchChain.invoke(input); + return VideoSearchChain.invoke(input, { ...getLangfuseCallbacks() }); }; export default handleVideoSearch; diff --git a/src/lib/search/simplifiedAgent.ts b/src/lib/search/simplifiedAgent.ts index 06666bb..9c90d8b 100644 --- a/src/lib/search/simplifiedAgent.ts +++ b/src/lib/search/simplifiedAgent.ts @@ -19,6 +19,7 @@ import { import { formatDateForLLM } from '../utils'; import { getModelName } from '../utils/modelUtils'; import { removeThinkingBlocks } from '../utils/contentUtils'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; /** * Normalize usage metadata from different LLM providers @@ -511,12 +512,14 @@ Use all available tools strategically to provide comprehensive, well-researched, }, recursionLimit: 25, // Allow sufficient iterations for tool use signal: this.signal, + ...getLangfuseCallbacks(), }; // Use streamEvents to capture both tool calls and token-level streaming const eventStream = agent.streamEvents(initialState, { ...config, version: 'v2', + ...getLangfuseCallbacks(), }); let finalResult: any = null; diff --git a/src/lib/search/speedSearch.ts b/src/lib/search/speedSearch.ts index b09a2b4..68fe6ce 100644 --- a/src/lib/search/speedSearch.ts +++ b/src/lib/search/speedSearch.ts @@ -23,6 +23,7 @@ import { formatDateForLLM } from '../utils'; import { getDocumentsFromLinks } from '../utils/documents'; import formatChatHistoryAsString from '../utils/formatHistory'; import { getModelName } from '../utils/modelUtils'; +import { getLangfuseCallbacks } from '@/lib/tracing/langfuse'; export interface SpeedSearchAgentType { searchAndAnswer: ( @@ -103,7 +104,7 @@ class SpeedSearchAgent implements SpeedSearchAgentType { this.emitProgress(emitter, 10, `Building search query`); - return RunnableSequence.from([ + return RunnableSequence.from([ PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt), llm, this.strParser, @@ -235,8 +236,8 @@ class SpeedSearchAgent implements SpeedSearchAgentType { Make sure to answer the query in the summary. - `, - { signal }, + `, + { signal, ...getLangfuseCallbacks() }, ); const document = new Document({ @@ -348,7 +349,7 @@ class SpeedSearchAgent implements SpeedSearchAgentType { date, systemInstructions, }, - { signal: options?.signal }, + { signal: options?.signal, ...getLangfuseCallbacks() }, ); query = searchRetrieverResult.query; @@ -379,6 +380,7 @@ class SpeedSearchAgent implements SpeedSearchAgentType { ) .withConfig({ runName: 'FinalSourceRetriever', + ...getLangfuseCallbacks(), }) .pipe(this.processDocs), }), @@ -391,6 +393,7 @@ class SpeedSearchAgent implements SpeedSearchAgentType { this.strParser, ]).withConfig({ runName: 'FinalResponseGenerator', + ...getLangfuseCallbacks(), }); } @@ -539,7 +542,7 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n