├── Knowmore
├── __init__.py
├── handlers
│ ├── __init__.py
│ ├── message_processor.py
│ └── stream_handler.py
├── services
│ ├── __init__.py
│ ├── ai_provider.py
│ ├── openai_service.py
│ ├── claude_service.py
│ ├── web_search_firecrawl.py
│ └── search_orchestrator.py
├── asgi.py
├── wsgi.py
├── urls.py
├── utils.py
├── sse.py
├── views.py
└── settings.py
├── ui
├── src
│ ├── vite-env.d.ts
│ ├── lib
│ │ └── utils.ts
│ ├── chat
│ │ ├── chat-header.tsx
│ │ ├── chat-input.tsx
│ │ ├── model-selector.tsx
│ │ ├── chat-source.tsx
│ │ └── chat-message.tsx
│ ├── main.tsx
│ ├── components
│ │ ├── ui
│ │ │ ├── textarea.tsx
│ │ │ ├── scroll-button.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── chat-container.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── code-block.tsx
│ │ │ ├── message.tsx
│ │ │ ├── markdown.tsx
│ │ │ ├── prompt-input.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ └── loader.tsx
│ │ └── chat-source-placeholder.tsx
│ ├── App.tsx
│ └── index.css
├── logo.png
├── dark-logo.png
├── tsconfig.json
├── .gitignore
├── components.json
├── index.html
├── eslint.config.js
├── tsconfig.node.json
├── vite.config.ts
├── tsconfig.app.json
├── public
│ └── vite.svg
├── package.json
└── README.md
├── demo-knowmore.png
├── static
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
└── site.webmanifest
├── .gitignore
├── requirements.txt
├── Makefile
├── Dockerfile
├── run_asgi.py
├── manage.py
├── templates
└── react_app.html
├── LICENSE
└── README.md
/Knowmore/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Knowmore/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Knowmore/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/ui/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/ui/logo.png
--------------------------------------------------------------------------------
/ui/dark-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/ui/dark-logo.png
--------------------------------------------------------------------------------
/demo-knowmore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/demo-knowmore.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/favicon-32x32.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | static/CACHE/
3 | static/dist/
4 | .venv/
5 | bun.lockb
6 | db.sqlite3
7 | .env
8 |
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmadrosid/Knowmore/HEAD/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | anthropic
2 | openai
3 | Django==5.2.3
4 | daphne==4.2.0
5 | django-compressor==4.5.1
6 | django-environ
7 | requests
8 |
--------------------------------------------------------------------------------
/ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/ui/src/chat/chat-header.tsx:
--------------------------------------------------------------------------------
1 | export function ChatHeader() {
2 | return (
3 |
4 |
5 | Knowmore
6 |
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | build: ui backend
4 |
5 | dev: ui backend run-backend
6 |
7 | ui:
8 | ./build_ui.sh
9 |
10 | serve-ui:
11 | cd ui && bun run dev
12 |
13 | backend:
14 | docker build . -t knowmore
15 |
16 | run-backend:
17 | docker run --env-file .env -p 7000:8000 knowmore
--------------------------------------------------------------------------------
/ui/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import '../dark-logo.png'
5 | import '../logo.png'
6 | import App from './App.tsx'
7 |
8 | createRoot(document.getElementById('root')!).render(
9 |
10 |
11 | ,
12 | )
13 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/Knowmore/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for Knowmore project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Knowmore.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/Knowmore/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for Knowmore project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Knowmore.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/ui/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-slim
2 |
3 | # Set environment variables
4 | ENV PYTHONDONTWRITEBYTECODE 1
5 | ENV PYTHONUNBUFFERED 1
6 |
7 | # Set work directory
8 | WORKDIR /app
9 |
10 | # Install dependencies
11 | COPY requirements.txt .
12 | RUN pip install --upgrade pip
13 | RUN pip install -r requirements.txt
14 |
15 | # Copy project
16 | COPY . .
17 |
18 | # Expose port
19 | EXPOSE 8000
20 |
21 | # Run the application with Daphne for proper streaming support
22 | CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "Knowmore.asgi:application"]
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Knowmore
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/run_asgi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Run the Django application with Daphne ASGI server for proper SSE streaming support.
4 | """
5 |
6 | import os
7 | import sys
8 |
9 | if __name__ == "__main__":
10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Knowmore.settings")
11 |
12 | # Run with Daphne
13 | from daphne.cli import CommandLineInterface
14 |
15 | # Run on port 8000
16 | sys.argv = ["daphne", "-b", "127.0.0.1", "-p", "8000", "Knowmore.asgi:application"]
17 |
18 | cli = CommandLineInterface()
19 | cli.run(sys.argv[1:])
--------------------------------------------------------------------------------
/Knowmore/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path
3 | from django.conf import settings
4 | from django.conf.urls.static import static
5 | from .views import index, sse_stream, get_manifest, get_models
6 |
7 | urlpatterns = [
8 | path('admin/', admin.site.urls),
9 | path('', index, name='react_app'),
10 | path('api/stream', sse_stream, name='sse_stream'),
11 | path('api/models', get_models, name='get_models'),
12 | path('manifest/', get_manifest, name='get_manifest'),
13 | ]
14 |
15 | if settings.DEBUG:
16 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
17 |
--------------------------------------------------------------------------------
/ui/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 | import { globalIgnores } from 'eslint/config'
7 |
8 | export default tseslint.config([
9 | globalIgnores(['dist']),
10 | {
11 | files: ['**/*.{ts,tsx}'],
12 | extends: [
13 | js.configs.recommended,
14 | tseslint.configs.recommended,
15 | reactHooks.configs['recommended-latest'],
16 | reactRefresh.configs.vite,
17 | ],
18 | languageOptions: {
19 | ecmaVersion: 2020,
20 | globals: globals.browser,
21 | },
22 | },
23 | ])
24 |
--------------------------------------------------------------------------------
/ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2023",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "verbatimModuleSyntax": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "erasableSyntaxOnly": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true
23 | },
24 | "include": ["vite.config.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Knowmore.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/ui/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import tailwindcss from '@tailwindcss/vite'
4 | import path from "path"
5 |
6 | export default defineConfig({
7 | plugins: [react(), tailwindcss()],
8 | resolve: {
9 | alias: {
10 | "@": path.resolve(__dirname, "./src"),
11 | },
12 | },
13 | build: {
14 | outDir: '../static/dist',
15 | assetsDir: 'assets',
16 | emptyOutDir: true,
17 | rollupOptions: {
18 | output: {
19 | entryFileNames: 'assets/[name]-[hash].js',
20 | chunkFileNames: 'assets/[name]-[hash].js',
21 | assetFileNames: 'assets/[name].[ext]'
22 | }
23 | }
24 | },
25 | server: {
26 | port: 3000,
27 | proxy: {
28 | "/api": {
29 | target: "http://127.0.0.1:7000",
30 | changeOrigin: true
31 | },
32 | },
33 | },
34 | base: '/static/dist/'
35 | })
36 |
--------------------------------------------------------------------------------
/ui/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2022",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "erasableSyntaxOnly": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noUncheckedSideEffectImports": true,
25 | "baseUrl": ".",
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | },
32 | "include": ["src"]
33 | }
34 |
--------------------------------------------------------------------------------
/Knowmore/handlers/message_processor.py:
--------------------------------------------------------------------------------
1 | def format_messages(messages):
2 | """Format messages to only include role and content (ignore parts)"""
3 | formatted_messages = []
4 | for msg in messages:
5 | if 'parts' in msg:
6 | # Extract text content from parts
7 | content = ""
8 | for part in msg['parts']:
9 | if isinstance(part, dict) and 'text' in part:
10 | content += part['text']
11 | elif isinstance(part, str):
12 | content += part
13 | formatted_messages.append({
14 | "role": msg['role'],
15 | "content": content
16 | })
17 | else:
18 | # Message already in correct format
19 | formatted_messages.append({
20 | "role": msg['role'],
21 | "content": msg.get('content', '')
22 | })
23 | return formatted_messages
--------------------------------------------------------------------------------
/Knowmore/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from django.conf import settings
4 |
5 | def get_vite_assets():
6 | """
7 | Parse the Vite-generated index.html to extract asset paths
8 | """
9 | try:
10 | vite_html_path = os.path.join(settings.BASE_DIR, 'static', 'dist', 'index.html')
11 |
12 | with open(vite_html_path, 'r') as f:
13 | content = f.read()
14 |
15 | # Extract CSS file
16 | css_match = re.search(r'href="([^"]*\.css)"', content)
17 | css_file = css_match.group(1) if css_match else None
18 |
19 | # Extract JS file
20 | js_match = re.search(r'src="([^"]*\.js)"', content)
21 | js_file = js_match.group(1) if js_match else None
22 |
23 | return {
24 | 'css': css_file.replace('/static/', '') if css_file else None,
25 | 'js': js_file.replace('/static/', '') if js_file else None
26 | }
27 | except FileNotFoundError:
28 | return {'css': None, 'js': None}
--------------------------------------------------------------------------------
/templates/react_app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% load static %}
6 |
7 |
8 |
9 | Knowmore
10 |
11 |
12 | {% if assets.css %}
13 |
14 | {% endif %}
15 |
16 |
17 |
18 | {% if assets.js %}
19 |
20 | {% else %}
21 |
22 |
Frontend not built
23 |
Please run: cd frontend && npm run build
24 |
25 | {% endif %}
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Ahmad Rosid
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/ui/src/components/ui/scroll-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button, buttonVariants } from "@/components/ui/button"
2 | import { cn } from "@/lib/utils"
3 | import { type VariantProps } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 | import { useStickToBottomContext } from "use-stick-to-bottom"
6 |
7 | export type ScrollButtonProps = {
8 | className?: string
9 | variant?: VariantProps["variant"]
10 | size?: VariantProps["size"]
11 | } & React.ButtonHTMLAttributes
12 |
13 | function ScrollButton({
14 | className,
15 | variant = "outline",
16 | size = "sm",
17 | ...props
18 | }: ScrollButtonProps) {
19 | const { isAtBottom, scrollToBottom } = useStickToBottomContext()
20 |
21 | return (
22 |
37 | )
38 | }
39 |
40 | export { ScrollButton }
41 |
--------------------------------------------------------------------------------
/ui/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/Knowmore/services/ai_provider.py:
--------------------------------------------------------------------------------
1 | from .claude_service import ClaudeService
2 | from .openai_service import OpenAIService
3 |
4 |
5 | class AIProviderFactory:
6 | @staticmethod
7 | def get_provider(model_name):
8 | if model_name.startswith('claude'):
9 | return ClaudeService()
10 | elif model_name.startswith('gpt') or model_name.startswith('o4'):
11 | return OpenAIService()
12 | else:
13 | # Default to Claude
14 | return ClaudeService()
15 |
16 | @staticmethod
17 | def get_supported_models():
18 | return {
19 | 'claude': [
20 | {'id': 'claude-opus-4-20250514', 'name': 'Claude Opus 4'},
21 | {'id': 'claude-sonnet-4-20250514', 'name': 'Claude Sonnet 4'},
22 | {'id': 'claude-3-7-sonnet-20250219', 'name': 'Claude Sonnet 3.7'},
23 | {'id': 'claude-3-5-sonnet-latest', 'name': 'Claude Sonnet 3.5'},
24 | {'id': 'claude-3-5-haiku-latest', 'name': 'Claude Haiku 3.5'},
25 | {'id': 'claude-3-5-sonnet-20240620', 'name': 'Claude Sonnet 3.5'}
26 | ],
27 | 'openai': [
28 | {'id': 'gpt-4o-2024-08-06', 'name': 'GPT-4o'},
29 | {'id': 'o4-mini-2025-04-16', 'name': 'O4 Mini'},
30 | {'id': 'gpt-4.1-2025-04-14', 'name': 'GPT-4.1'}
31 | ]
32 | }
--------------------------------------------------------------------------------
/ui/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Knowmore/services/openai_service.py:
--------------------------------------------------------------------------------
1 | import json
2 | import environ
3 | from openai import AsyncOpenAI
4 |
5 | env = environ.Env(
6 | OPENAI_API_KEY=str,
7 | )
8 |
9 | class OpenAIService:
10 | def __init__(self):
11 | self.client = AsyncOpenAI(
12 | api_key=env("OPENAI_API_KEY"),
13 | )
14 |
15 | async def stream_response(self, messages, model="gpt-3.5-turbo", enable_web_search=False):
16 | """Stream chat completion response without tool support"""
17 | try:
18 | stream_params = {
19 | "model": model,
20 | "messages": messages,
21 | "stream": True,
22 | "max_tokens": 1024,
23 | }
24 |
25 | stream = await self.client.chat.completions.create(**stream_params)
26 |
27 | async for chunk in stream:
28 | choice = chunk.choices[0]
29 | delta = choice.delta
30 |
31 | # Handle regular text content
32 | if delta.content is not None:
33 | yield f'0:{json.dumps(delta.content)}\n'
34 |
35 | # Handle completion
36 | if choice.finish_reason is not None:
37 | yield f'd:{json.dumps({"finishReason": choice.finish_reason})}\n'
38 |
39 | except Exception as e:
40 | # Send error using 3: identifier
41 | yield f'3:{json.dumps({"error": str(e)})}\n'
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@ai-sdk/react": "^1.2.12",
14 | "@radix-ui/react-avatar": "^1.1.10",
15 | "@radix-ui/react-dropdown-menu": "^2.1.15",
16 | "@radix-ui/react-scroll-area": "^1.2.9",
17 | "@radix-ui/react-slot": "^1.2.3",
18 | "@radix-ui/react-tooltip": "^1.2.7",
19 | "@tailwindcss/typography": "^0.5.16",
20 | "@tailwindcss/vite": "^4.1.11",
21 | "class-variance-authority": "^0.7.1",
22 | "clsx": "^2.1.1",
23 | "lucide-react": "^0.523.0",
24 | "marked": "^16.0.0",
25 | "react": "^19.1.0",
26 | "react-dom": "^19.1.0",
27 | "react-markdown": "^10.1.0",
28 | "remark-breaks": "^4.0.0",
29 | "remark-gfm": "^4.0.1",
30 | "shiki": "^3.7.0",
31 | "swr": "^2.3.3",
32 | "tailwind-merge": "^3.3.1",
33 | "tailwindcss": "^4.1.11",
34 | "tailwindcss-animate": "^1.0.7",
35 | "use-stick-to-bottom": "^1.1.1"
36 | },
37 | "devDependencies": {
38 | "@eslint/js": "^9.29.0",
39 | "@types/node": "^24.0.4",
40 | "@types/react": "^19.1.8",
41 | "@types/react-dom": "^19.1.6",
42 | "@vitejs/plugin-react": "^4.5.2",
43 | "eslint": "^9.29.0",
44 | "eslint-plugin-react-hooks": "^5.2.0",
45 | "eslint-plugin-react-refresh": "^0.4.20",
46 | "globals": "^16.2.0",
47 | "tw-animate-css": "^1.3.4",
48 | "typescript": "~5.8.3",
49 | "typescript-eslint": "^8.34.1",
50 | "vite": "^7.0.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ui/src/components/ui/chat-container.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { StickToBottom } from "use-stick-to-bottom"
3 |
4 | export type ChatContainerRootProps = {
5 | children: React.ReactNode
6 | className?: string
7 | } & React.HTMLAttributes
8 |
9 | export type ChatContainerContentProps = {
10 | children: React.ReactNode
11 | className?: string
12 | } & React.HTMLAttributes
13 |
14 | export type ChatContainerScrollAnchorProps = {
15 | className?: string
16 | ref?: React.RefObject
17 | } & React.HTMLAttributes
18 |
19 | function ChatContainerRoot({
20 | children,
21 | className,
22 | ...props
23 | }: ChatContainerRootProps) {
24 | return (
25 |
32 | {children}
33 |
34 | )
35 | }
36 |
37 | function ChatContainerContent({
38 | children,
39 | className,
40 | ...props
41 | }: ChatContainerContentProps) {
42 | return (
43 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | function ChatContainerScrollAnchor({
53 | className,
54 | ...props
55 | }: ChatContainerScrollAnchorProps) {
56 | return (
57 |
62 | )
63 | }
64 |
65 | export { ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchor }
66 |
--------------------------------------------------------------------------------
/ui/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/ui/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function ScrollArea({
7 | className,
8 | children,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
17 |
21 | {children}
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function ScrollBar({
30 | className,
31 | orientation = "vertical",
32 | ...props
33 | }: React.ComponentProps) {
34 | return (
35 |
48 |
52 |
53 | )
54 | }
55 |
56 | export { ScrollArea, ScrollBar }
57 |
--------------------------------------------------------------------------------
/Knowmore/sse.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from django.http import StreamingHttpResponse, JsonResponse
3 |
4 | from .services.ai_provider import AIProviderFactory
5 |
6 | class SSEResponse(StreamingHttpResponse):
7 | def __init__(self, streaming_content=(), *args, **kwargs):
8 | sync_streaming_content = self.get_sync_iterator(streaming_content)
9 | super().__init__(streaming_content=sync_streaming_content, *args, **kwargs)
10 |
11 | @staticmethod
12 | async def convert_async_iterable(stream):
13 | """Accepts async_generator and async_iterator"""
14 | return [chunk async for chunk in stream]
15 |
16 | def get_sync_iterator(self, async_iterable):
17 | try:
18 | # Try to get the current event loop
19 | loop = asyncio.get_running_loop()
20 | except RuntimeError:
21 | # No event loop running, create a new one
22 | loop = asyncio.new_event_loop()
23 | asyncio.set_event_loop(loop)
24 | result = loop.run_until_complete(self.convert_async_iterable(async_iterable))
25 | loop.close()
26 | return iter(result)
27 | else:
28 | # Event loop is already running, use asyncio.create_task or run_in_executor
29 | import concurrent.futures
30 | import threading
31 |
32 | def run_async_in_thread():
33 | new_loop = asyncio.new_event_loop()
34 | asyncio.set_event_loop(new_loop)
35 | try:
36 | result = new_loop.run_until_complete(self.convert_async_iterable(async_iterable))
37 | return result
38 | finally:
39 | new_loop.close()
40 |
41 | with concurrent.futures.ThreadPoolExecutor() as executor:
42 | future = executor.submit(run_async_in_thread)
43 | result = future.result()
44 | return iter(result)
45 |
--------------------------------------------------------------------------------
/ui/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/ui/src/chat/chat-input.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | PromptInput,
5 | PromptInputAction,
6 | PromptInputActions,
7 | PromptInputTextarea,
8 | } from "@/components/ui/prompt-input"
9 | import { Button } from "@/components/ui/button"
10 | import { ArrowUp, Square } from "lucide-react"
11 | import { type ChatRequestOptions } from "@ai-sdk/ui-utils";
12 | import { cn } from "@/lib/utils";
13 | import { ModelSelector } from "./model-selector";
14 |
15 | interface ChatInputProps {
16 | input: string;
17 | status: "submitted" | "streaming" | "ready" | "error";
18 | setInput: (value: string) => void;
19 | handleSubmit: (event?: {
20 | preventDefault?: () => void;
21 | }, chatRequestOptions?: ChatRequestOptions) => void;
22 | selectedModel: string;
23 | onModelChange: (modelId: string) => void;
24 | }
25 |
26 | export function ChatInput({ input, setInput, handleSubmit, status, selectedModel, onModelChange }: ChatInputProps) {
27 |
28 | const isLoading = status === "streaming" || status === "submitted";
29 |
30 | return (
31 | setInput(value)}
34 | isLoading={isLoading}
35 | onSubmit={handleSubmit}
36 | className="w-full max-w-(--breakpoint-md)"
37 | >
38 |
39 |
40 |
41 |
44 |
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Knowmore
2 |
3 | **Knowmore** is a clone of Perplexity AI, it has a lot of thing missing features. The goals for this project is to learn django and RAG architecture.
4 |
5 | 
6 |
7 | ## Demo
8 |
9 | Watch the YouTube demo: https://www.youtube.com/watch?v=_mFrt65fLQA
10 |
11 | ## Requirements
12 |
13 | - Python >= 3.10
14 | - Node.js/Bun
15 | - Docker (optional)
16 |
17 | **Environment Variables**:
18 |
19 | ```bash
20 | ANTHROPIC_API_KEY=
21 | OPENAI_API_KEY=
22 | FIRE_CRAWL_API_TOKEN=
23 | SECRET_KEY=
24 | ```
25 |
26 | ## Integration
27 |
28 | For now it integrates with Anthropic AI. Soon will be adding OpenAI GPT-4 and other LLMs.
29 |
30 | ## Quick Start
31 |
32 | ### Using Make (Recommended)
33 |
34 | Build and run the full application:
35 | ```bash
36 | make dev
37 | ```
38 |
39 | This will:
40 | - Build the frontend using `./build_frontend.sh`
41 | - Build the Docker image
42 | - Run the backend on port 7000
43 |
44 | Individual commands:
45 | ```bash
46 | # Build frontend and backend
47 | make build
48 |
49 | # Serve frontend only (for development)
50 | make serve-ui
51 |
52 | # Build Docker image only
53 | make backend
54 |
55 | # Run backend container only
56 | make run-backend
57 | ```
58 |
59 | ### Manual Setup
60 |
61 | Install Python packages:
62 | ```bash
63 | pip install -r requirements.txt
64 | ```
65 |
66 | Install frontend dependencies:
67 | ```bash
68 | cd frontend && bun install
69 | ```
70 |
71 | Run Python server:
72 |
73 | For development (no streaming support):
74 | ```bash
75 | python manage.py runserver
76 | ```
77 |
78 | For proper SSE streaming support (recommended):
79 | ```bash
80 | python run_asgi.py
81 | # or
82 | daphne -b 127.0.0.1 -p 8000 Knowmore.asgi:application
83 | ```
84 |
85 | ### Using Docker
86 |
87 | Build Docker image:
88 | ```bash
89 | docker build . -t knowmore
90 | ```
91 |
92 | Run Docker container:
93 | ```bash
94 | docker run --env-file .env -p 7000:8000 knowmore
95 | ```
96 |
97 | Access the application at: http://localhost:7000
98 |
99 | ## Credits
100 |
101 | This project is inspired by this company.
102 |
103 | - [Devv AI](https://devv.ai/)
104 | - [Perplexity](https://www.perplexity.ai/)
105 |
--------------------------------------------------------------------------------
/Knowmore/services/claude_service.py:
--------------------------------------------------------------------------------
1 | import json
2 | import environ
3 | from anthropic import AsyncAnthropic
4 |
5 | env = environ.Env(
6 | ANTHROPIC_API_KEY=str,
7 | )
8 |
9 | class ClaudeService:
10 | def __init__(self):
11 | self.client = AsyncAnthropic(
12 | api_key=env("ANTHROPIC_API_KEY"),
13 | )
14 |
15 | async def stream_response(self, messages, model="claude-3-5-sonnet-20240620", enable_web_search=False):
16 | stream_params = {
17 | "max_tokens": 1024,
18 | "messages": messages,
19 | "model": model,
20 | }
21 |
22 | try:
23 | async with self.client.messages.stream(**stream_params) as stream:
24 | async for event in stream:
25 | # Handle different event types from Claude's streaming
26 | if hasattr(event, 'type'):
27 | if event.type == 'content_block_delta':
28 | if hasattr(event.delta, 'text'):
29 | # Stream text using Vercel protocol
30 | yield f'0:{json.dumps(event.delta.text)}\n'
31 |
32 | elif event.type == 'content_block_start':
33 | # Handle initial text content
34 | if hasattr(event, 'content_block') and hasattr(event.content_block, 'type'):
35 | if event.content_block.type == 'text' and hasattr(event.content_block, 'text'):
36 | if event.content_block.text:
37 | yield f'0:{json.dumps(event.content_block.text)}\n'
38 |
39 | elif event.type == 'message_stop':
40 | # Send message finish with d: identifier
41 | yield f'd:{json.dumps({"finishReason": "stop"})}\n'
42 |
43 | else:
44 | # Fallback for text content
45 | if hasattr(event, 'text'):
46 | yield f'0:{json.dumps(event.text)}\n'
47 |
48 | except Exception as e:
49 | # Send error using 3: identifier
50 | yield f'3:{json.dumps({"error": str(e)})}\n'
--------------------------------------------------------------------------------
/ui/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/ui/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/ui/src/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import React, { useEffect, useState } from "react"
3 | import { codeToHtml } from "shiki"
4 |
5 | export type CodeBlockProps = {
6 | children?: React.ReactNode
7 | className?: string
8 | } & React.HTMLProps
9 |
10 | function CodeBlock({ children, className, ...props }: CodeBlockProps) {
11 | return (
12 |
20 | {children}
21 |
22 | )
23 | }
24 |
25 | export type CodeBlockCodeProps = {
26 | code: string
27 | language?: string
28 | theme?: string
29 | className?: string
30 | } & React.HTMLProps
31 |
32 | function CodeBlockCode({
33 | code,
34 | language = "tsx",
35 | theme = "github-light",
36 | className,
37 | ...props
38 | }: CodeBlockCodeProps) {
39 | const [highlightedHtml, setHighlightedHtml] = useState(null)
40 |
41 | useEffect(() => {
42 | async function highlight() {
43 | if (!code) {
44 | setHighlightedHtml("
")
45 | return
46 | }
47 |
48 | const html = await codeToHtml(code, { lang: language, theme })
49 | setHighlightedHtml(html)
50 | }
51 | highlight()
52 | }, [code, language, theme])
53 |
54 | const classNames = cn(
55 | "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
56 | className
57 | )
58 |
59 | // SSR fallback: render plain code if not hydrated yet
60 | return highlightedHtml ? (
61 |
66 | ) : (
67 |
68 |
69 | {code}
70 |
71 |
72 | )
73 | }
74 |
75 | export type CodeBlockGroupProps = React.HTMLAttributes
76 |
77 | function CodeBlockGroup({
78 | children,
79 | className,
80 | ...props
81 | }: CodeBlockGroupProps) {
82 | return (
83 |
87 | {children}
88 |
89 | )
90 | }
91 |
92 | export { CodeBlockGroup, CodeBlockCode, CodeBlock }
93 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13 |
14 | ```js
15 | export default tseslint.config([
16 | globalIgnores(['dist']),
17 | {
18 | files: ['**/*.{ts,tsx}'],
19 | extends: [
20 | // Other configs...
21 |
22 | // Remove tseslint.configs.recommended and replace with this
23 | ...tseslint.configs.recommendedTypeChecked,
24 | // Alternatively, use this for stricter rules
25 | ...tseslint.configs.strictTypeChecked,
26 | // Optionally, add this for stylistic rules
27 | ...tseslint.configs.stylisticTypeChecked,
28 |
29 | // Other configs...
30 | ],
31 | languageOptions: {
32 | parserOptions: {
33 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
34 | tsconfigRootDir: import.meta.dirname,
35 | },
36 | // other options...
37 | },
38 | },
39 | ])
40 | ```
41 |
42 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
43 |
44 | ```js
45 | // eslint.config.js
46 | import reactX from 'eslint-plugin-react-x'
47 | import reactDom from 'eslint-plugin-react-dom'
48 |
49 | export default tseslint.config([
50 | globalIgnores(['dist']),
51 | {
52 | files: ['**/*.{ts,tsx}'],
53 | extends: [
54 | // Other configs...
55 | // Enable lint rules for React
56 | reactX.configs['recommended-typescript'],
57 | // Enable lint rules for React DOM
58 | reactDom.configs.recommended,
59 | ],
60 | languageOptions: {
61 | parserOptions: {
62 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
63 | tsconfigRootDir: import.meta.dirname,
64 | },
65 | // other options...
66 | },
67 | },
68 | ])
69 | ```
70 |
--------------------------------------------------------------------------------
/Knowmore/views.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 |
4 | from django.http import StreamingHttpResponse, JsonResponse
5 | from django.shortcuts import render
6 |
7 | from .utils import get_vite_assets
8 | from .handlers.stream_handler import event_stream, error_stream
9 | from .handlers.message_processor import format_messages
10 | from .services.ai_provider import AIProviderFactory
11 |
12 | def index(request):
13 | assets = get_vite_assets()
14 | return render(request, "react_app.html", {"assets": assets})
15 |
16 |
17 | async def sse_stream(request):
18 | if request.method != 'POST':
19 | return StreamingHttpResponse(error_stream('Method not allowed'), content_type='text/event-stream', status=405)
20 |
21 | try:
22 | body = json.loads(request.body)
23 | messages = body.get('messages', [])
24 | model = body.get('model', 'claude-3-5-sonnet-20240620')
25 | enable_web_search = body.get('enable_web_search', False)
26 |
27 | if not messages:
28 | return StreamingHttpResponse(error_stream('No messages found'), content_type='text/event-stream', status=400)
29 |
30 | input_messages = format_messages(messages)
31 | response = StreamingHttpResponse(
32 | event_stream(input_messages, model, enable_web_search=enable_web_search),
33 | content_type='text/event-stream'
34 | )
35 | response['x-vercel-ai-data-stream'] = 'v1'
36 | return response
37 | except json.JSONDecodeError:
38 | return StreamingHttpResponse(error_stream('Invalid JSON'), content_type='text/event-stream', status=400)
39 |
40 | def get_models(request):
41 | """Get available models from all providers"""
42 | models_dict = AIProviderFactory.get_supported_models()
43 |
44 | # Transform into a flat list with provider info
45 | models = []
46 | for provider, model_list in models_dict.items():
47 | for model in model_list:
48 | models.append({
49 | 'id': model['id'],
50 | 'name': model['name'],
51 | 'provider': provider
52 | })
53 |
54 | return JsonResponse({'models': models})
55 |
56 |
57 | def get_manifest(request):
58 | """Read Vite manifest to get the correct asset paths"""
59 | try:
60 | manifest_path = os.path.join(os.path.dirname(__file__), '..', 'static', 'dist', '.vite', 'manifest.json')
61 | with open(manifest_path, 'r') as f:
62 | manifest = json.load(f)
63 | return JsonResponse(manifest)
64 | except FileNotFoundError:
65 | return JsonResponse({'error': 'Manifest not found. Please build the frontend first.'}, status=404)
66 |
--------------------------------------------------------------------------------
/ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { ChatInput } from '@/chat/chat-input'
3 | import { useChat } from '@ai-sdk/react';
4 | import { ChatMessage } from '@/chat/chat-message';
5 | import { ChatHeader } from '@/chat/chat-header';
6 | import {
7 | ChatContainerContent,
8 | ChatContainerRoot,
9 | ChatContainerScrollAnchor,
10 | } from "@/components/ui/chat-container"
11 | import { ScrollButton } from "@/components/ui/scroll-button"
12 | import { useState } from 'react';
13 |
14 | export default function App() {
15 | const [selectedModel, setSelectedModel] = useState(() => {
16 | const stored = localStorage.getItem('selectedModel');
17 | return stored || "claude-3-5-sonnet-latest";
18 | });
19 |
20 | const handleModelChange = (modelId: string) => {
21 | setSelectedModel(modelId);
22 | localStorage.setItem('selectedModel', modelId);
23 | };
24 |
25 | const { messages, input, setInput, status, handleSubmit } = useChat({
26 | api: "/api/stream",
27 | body: {
28 | model: selectedModel,
29 | enable_web_search: true,
30 | },
31 | // maxSteps removed - search is handled server-side in stream handler
32 | });
33 |
34 | return (
35 |
36 | 0 && "h-[95vh]",
39 | "max-w-3xl w-full mx-auto px-12 flex flex-col relative transition-all duration-500 ease-in-out"
40 | )}
41 | >
42 | {messages.length > 0 &&
}
43 |
0 || status === "submitted") ? "flex-1 w-full" : "hidden",
45 | "scrollbar-hidden transition-opacity duration-500 ease-in-out"
46 | )}>
47 | 0 ? "mb-32" : ""}>
48 |
49 |
50 |
51 |
56 |
57 |
0
61 | ? "absolute bottom-0 left-0 right-0 transform translate-y-0 transition-all duration-500 delay-300 ease-in-out"
62 | : "transform -translate-y-20"
63 | )}
64 | >
65 | {messages.length === 0 && }
66 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/ui/src/chat/model-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Brain, Check } from "lucide-react"
4 | import { Button } from "@/components/ui/button"
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuLabel,
9 | DropdownMenuItem,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import useSWR from "swr"
14 |
15 | export type ModelProvider = "claude" | "openai"
16 |
17 | export interface Model {
18 | id: string
19 | name: string
20 | provider: ModelProvider
21 | }
22 |
23 | const fetcher = (url: string) => fetch(url).then(res => res.json())
24 |
25 | interface ModelSelectorProps {
26 | selectedModel: string
27 | onModelChange: (modelId: string) => void
28 | }
29 |
30 | export function ModelSelector({ selectedModel, onModelChange }: ModelSelectorProps) {
31 | const { data, error, isLoading } = useSWR<{ models: Model[] }>('/api/models', fetcher, {
32 | revalidateOnFocus: false,
33 | revalidateOnReconnect: false,
34 | })
35 |
36 | const models = data?.models || []
37 | const currentModel = models.find(m => m.id === selectedModel) || models[0]
38 |
39 | return (
40 |
41 |
42 |
45 |
46 |
47 |
48 | {error ? "Error loading models" : "Select Model"}
49 |
50 |
51 | {error ? (
52 |
53 | Failed to load models
54 |
55 | ) : isLoading ? (
56 |
57 | Loading models...
58 |
59 | ) : models.length === 0 ? (
60 |
61 | No models available
62 |
63 | ) : (
64 | models.map(model => (
65 | onModelChange(model.id)}
68 | className="text-sm justify-between cursor-pointer"
69 | >
70 | {model.name}
71 | {currentModel?.id === model.id && }
72 |
73 | ))
74 | )}
75 |
76 |
77 | )
78 | }
--------------------------------------------------------------------------------
/Knowmore/handlers/stream_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import uuid
3 | from ..services.ai_provider import AIProviderFactory
4 | from ..services.search_orchestrator import SearchOrchestrator
5 |
6 |
7 | async def event_stream(messages, model, enable_web_search=True):
8 | """Simplified stream handler with SearchOrchestrator"""
9 | try:
10 | # Early return for non-search requests
11 | if not enable_web_search:
12 | provider = AIProviderFactory.get_provider(model)
13 | async for chunk in provider.stream_response(messages, model, enable_web_search=False):
14 | yield chunk
15 | return
16 |
17 | # Initialize search orchestrator
18 | search_orchestrator = SearchOrchestrator()
19 |
20 | # Generate multiple search queries
21 | search_queries = await search_orchestrator.generate_search_queries(messages)
22 |
23 | if search_queries:
24 | # Generate tool call IDs for each query upfront
25 | tool_call_ids = [f"search_{uuid.uuid4().hex[:8]}" for _ in search_queries]
26 |
27 | # Stream search indicators for each query
28 | for i, (query, tool_call_id) in enumerate(zip(search_queries, tool_call_ids)):
29 | # Stream search start indicator
30 | yield f'b:{json.dumps({"toolCallId": tool_call_id, "toolName": "web_search"})}\n'
31 |
32 | # Stream search query and execution indicators
33 | yield f'c:{json.dumps({"toolCallId": tool_call_id, "argsTextDelta": query})}\n'
34 | yield f'9:{json.dumps({"toolCallId": tool_call_id, "toolName": "web_search", "args": {"query": query}})}\n'
35 |
36 | # Execute all searches concurrently
37 | search_results_list = await search_orchestrator.execute_multiple_searches(search_queries)
38 |
39 | # Stream search results for each search with matching tool call IDs
40 | for i, (search_results, tool_call_id) in enumerate(zip(search_results_list, tool_call_ids)):
41 | yield f'a:{json.dumps({"toolCallId": tool_call_id, "result": search_results})}\n'
42 |
43 | # Enhance messages with all search contexts
44 | enhanced_messages = search_orchestrator.enhance_messages_with_multiple_searches(
45 | messages, search_results_list
46 | )
47 | else:
48 | enhanced_messages = messages
49 |
50 | # Stream AI response
51 | provider = AIProviderFactory.get_provider(model)
52 | async for chunk in provider.stream_response(enhanced_messages, model, enable_web_search=False):
53 | yield chunk
54 |
55 | except Exception as e:
56 | # Log the error and stream it
57 | print(f"Error in event_stream: {str(e)}")
58 | import traceback
59 | traceback.print_exc()
60 | yield f'3:{json.dumps({"error": f"Stream error: {str(e)}"})}\n'
61 |
62 |
63 |
64 |
65 | async def error_stream(message):
66 | """Generate error stream response using Vercel protocol"""
67 | yield f'3:{json.dumps({"error": message})}\n'
--------------------------------------------------------------------------------
/ui/src/components/ui/message.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip"
8 | import { cn } from "@/lib/utils"
9 | import { Markdown } from "./markdown"
10 |
11 | export type MessageProps = {
12 | children: React.ReactNode
13 | className?: string
14 | } & React.HTMLProps
15 |
16 | const Message = ({ children, className, ...props }: MessageProps) => (
17 |
18 | {children}
19 |
20 | )
21 |
22 | export type MessageAvatarProps = {
23 | src: string
24 | alt: string
25 | fallback?: string
26 | delayMs?: number
27 | className?: string
28 | }
29 |
30 | const MessageAvatar = ({
31 | src,
32 | alt,
33 | fallback,
34 | delayMs,
35 | className,
36 | }: MessageAvatarProps) => {
37 | return (
38 |
39 |
40 | {fallback && (
41 | {fallback}
42 | )}
43 |
44 | )
45 | }
46 |
47 | export type MessageContentProps = {
48 | children: React.ReactNode
49 | markdown?: boolean
50 | className?: string
51 | } & React.ComponentProps &
52 | React.HTMLProps
53 |
54 | const MessageContent = ({
55 | children,
56 | markdown = false,
57 | className,
58 | ...props
59 | }: MessageContentProps) => {
60 | const classNames = cn(
61 | "rounded-lg p-2 text-foreground bg-secondary prose break-words whitespace-normal",
62 | className
63 | )
64 |
65 | return markdown ? (
66 |
67 | {children as string}
68 |
69 | ) : (
70 |
71 | {children}
72 |
73 | )
74 | }
75 |
76 | export type MessageActionsProps = {
77 | children: React.ReactNode
78 | className?: string
79 | } & React.HTMLProps
80 |
81 | const MessageActions = ({
82 | children,
83 | className,
84 | ...props
85 | }: MessageActionsProps) => (
86 |
90 | {children}
91 |
92 | )
93 |
94 | export type MessageActionProps = {
95 | className?: string
96 | tooltip: React.ReactNode
97 | children: React.ReactNode
98 | side?: "top" | "bottom" | "left" | "right"
99 | } & React.ComponentProps
100 |
101 | const MessageAction = ({
102 | tooltip,
103 | children,
104 | className,
105 | side = "top",
106 | ...props
107 | }: MessageActionProps) => {
108 | return (
109 |
110 |
111 | {children}
112 |
113 | {tooltip}
114 |
115 |
116 |
117 | )
118 | }
119 |
120 | export { Message, MessageAvatar, MessageContent, MessageActions, MessageAction }
121 |
--------------------------------------------------------------------------------
/ui/src/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { marked } from "marked"
3 | import { memo, useId, useMemo } from "react"
4 | import ReactMarkdown, { type Components } from "react-markdown"
5 | import remarkBreaks from "remark-breaks"
6 | import remarkGfm from "remark-gfm"
7 | import { CodeBlock, CodeBlockCode } from "./code-block"
8 |
9 | export type MarkdownProps = {
10 | children: string
11 | id?: string
12 | className?: string
13 | components?: Partial
14 | }
15 |
16 | function parseMarkdownIntoBlocks(markdown: string): string[] {
17 | const tokens = marked.lexer(markdown)
18 | return tokens.map((token) => token.raw)
19 | }
20 |
21 | function extractLanguage(className?: string): string {
22 | if (!className) return "plaintext"
23 | const match = className.match(/language-(\w+)/)
24 | return match ? match[1] : "plaintext"
25 | }
26 |
27 | const INITIAL_COMPONENTS: Partial = {
28 | code: function CodeComponent({ className, children, ...props }) {
29 | const isInline =
30 | !props.node?.position?.start.line ||
31 | props.node?.position?.start.line === props.node?.position?.end.line
32 |
33 | if (isInline) {
34 | return (
35 |
42 | {children}
43 |
44 | )
45 | }
46 |
47 | const language = extractLanguage(className)
48 |
49 | return (
50 |
51 |
52 |
53 | )
54 | },
55 | pre: function PreComponent({ children }) {
56 | return <>{children}>
57 | },
58 | }
59 |
60 | const MemoizedMarkdownBlock = memo(
61 | function MarkdownBlock({
62 | content,
63 | components = INITIAL_COMPONENTS,
64 | }: {
65 | content: string
66 | components?: Partial
67 | }) {
68 | return (
69 |
73 | {content}
74 |
75 | )
76 | },
77 | function propsAreEqual(prevProps, nextProps) {
78 | return prevProps.content === nextProps.content
79 | }
80 | )
81 |
82 | MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock"
83 |
84 | function MarkdownComponent({
85 | children,
86 | id,
87 | className,
88 | components = INITIAL_COMPONENTS,
89 | }: MarkdownProps) {
90 | const generatedId = useId()
91 | const blockId = id ?? generatedId
92 | const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])
93 |
94 | return (
95 |
96 | {blocks.map((block, index) => (
97 |
102 | ))}
103 |
104 | )
105 | }
106 |
107 | const Markdown = memo(MarkdownComponent)
108 | Markdown.displayName = "Markdown"
109 |
110 | export { Markdown }
111 |
--------------------------------------------------------------------------------
/ui/src/components/chat-source-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import { ScrollArea } from "@/components/ui/scroll-area";
3 |
4 | export function ChatSourcePlaceholder() {
5 | return (
6 |
7 | {/* Header Section Shimmer */}
8 |
12 |
13 | {/* Filter Tags Shimmer */}
14 |
15 | {[...Array(2)].map((_, index) => (
16 |
20 | ))}
21 |
22 |
23 | {/* Scrollable Results Shimmer */}
24 |
25 |
26 | {[...Array(3)].map((_, index) => (
27 |
31 |
32 |
33 | {/* Favicon shimmer */}
34 |
35 |
36 |
37 | {/* Title shimmer */}
38 |
39 | {/* URL shimmer */}
40 |
41 |
42 |
43 |
44 |
45 | {/* Content shimmer */}
46 |
47 | {/* Preview shimmer */}
48 |
53 |
54 |
55 |
56 | ))}
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/Knowmore/settings.py:
--------------------------------------------------------------------------------
1 |
2 | from pathlib import Path
3 | import environ
4 |
5 | env = environ.Env(
6 | SECRET_KEY=str,
7 | )
8 |
9 | BASE_DIR = Path(__file__).resolve().parent.parent
10 | SECRET_KEY = env("SECRET_KEY")
11 | DEBUG = True
12 | ALLOWED_HOSTS = []
13 |
14 |
15 | # Application definition
16 |
17 | INSTALLED_APPS = [
18 | 'daphne',
19 | 'django.contrib.admin',
20 | 'django.contrib.auth',
21 | 'django.contrib.contenttypes',
22 | 'django.contrib.sessions',
23 | 'django.contrib.messages',
24 | 'django.contrib.staticfiles',
25 | 'compressor',
26 | ]
27 |
28 | MIDDLEWARE = [
29 | 'django.middleware.security.SecurityMiddleware',
30 | 'django.contrib.sessions.middleware.SessionMiddleware',
31 | 'django.middleware.common.CommonMiddleware',
32 | # 'django.middleware.csrf.CsrfViewMiddleware',
33 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
34 | 'django.contrib.messages.middleware.MessageMiddleware',
35 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
36 | ]
37 |
38 | ASGI_APPLICATION = "Knowmore.asgi.application"
39 |
40 | ROOT_URLCONF = 'Knowmore.urls'
41 |
42 | TEMPLATES = [
43 | {
44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
45 | 'DIRS': [BASE_DIR / 'templates'],
46 | 'APP_DIRS': True,
47 | 'OPTIONS': {
48 | 'context_processors': [
49 | 'django.template.context_processors.debug',
50 | 'django.template.context_processors.request',
51 | 'django.contrib.auth.context_processors.auth',
52 | 'django.contrib.messages.context_processors.messages',
53 | ],
54 | },
55 | },
56 | ]
57 |
58 | WSGI_APPLICATION = 'Knowmore.wsgi.application'
59 |
60 |
61 | # Database
62 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
63 |
64 | DATABASES = {
65 | 'default': {
66 | 'ENGINE': 'django.db.backends.sqlite3',
67 | 'NAME': BASE_DIR / 'db.sqlite3',
68 | }
69 | }
70 |
71 |
72 | # Password validation
73 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
74 |
75 | AUTH_PASSWORD_VALIDATORS = [
76 | {
77 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
78 | },
79 | {
80 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
81 | },
82 | {
83 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
84 | },
85 | {
86 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
87 | },
88 | ]
89 |
90 |
91 | # Internationalization
92 | # https://docs.djangoproject.com/en/4.2/topics/i18n/
93 |
94 | LANGUAGE_CODE = 'en-us'
95 |
96 | TIME_ZONE = 'UTC'
97 |
98 | USE_I18N = True
99 |
100 | USE_TZ = True
101 |
102 | STATIC_URL = '/static/'
103 | STATICFILES_DIRS = [
104 | BASE_DIR / 'static',
105 | ]
106 | STATIC_ROOT = BASE_DIR / 'staticfiles'
107 |
108 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
109 |
110 | COMPRESS_ROOT = BASE_DIR / 'static'
111 |
112 | COMPRESS_ENABLED = True
113 |
114 | STATICFILES_FINDERS = (
115 | 'django.contrib.staticfiles.finders.FileSystemFinder',
116 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
117 | 'compressor.finders.CompressorFinder',
118 | )
119 |
--------------------------------------------------------------------------------
/ui/src/chat/chat-source.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import { Badge } from "@/components/ui/badge";
3 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
4 | import { ChatSourcePlaceholder } from "@/components/chat-source-placeholder";
5 |
6 | interface SearchResult {
7 | id: string;
8 | title: string;
9 | url: string;
10 | preview: string;
11 | favicon: string;
12 | domain: string;
13 | }
14 |
15 | type ChatSourceProps = {
16 | filterTags: string[];
17 | searchResults: SearchResult[]
18 | }
19 |
20 | export function ChatSource({ filterTags, searchResults }: ChatSourceProps) {
21 | if (searchResults.length === 0) {
22 | return
23 | }
24 |
25 | return (
26 |
27 | {/* Header Section */}
28 |
29 |
Sources
30 | {searchResults.length} Results
31 |
32 |
33 | {/* Filter Tags */}
34 |
35 | {filterTags.map((tag, index) => (
36 |
41 | {tag}
42 |
43 | ))}
44 |
45 |
46 | {/* Scrollable Results */}
47 |
48 |
49 | {searchResults.length === 0 ? (
50 |
51 | ) : (
52 | searchResults.map((result) => (
53 |
window.open(result.url, '_blank', 'noopener,noreferrer')}
57 | >
58 |
59 |
60 | {/* Favicon */}
61 |
62 |
63 | {result.favicon}
64 |
65 |
66 | {/* Title */}
67 |
68 | {result.title}
69 |
70 |
71 | {/* URL */}
72 |
73 | {result.domain}
74 |
75 |
76 |
77 |
78 |
79 | {/* Content */}
80 |
81 | {/* Preview */}
82 |
83 | {result.preview}
84 |
85 |
86 |
87 |
88 | ))
89 | )}
90 |
91 |
92 |
93 |
94 | );
95 | }
--------------------------------------------------------------------------------
/Knowmore/services/web_search_firecrawl.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 | import environ
4 | from typing import Dict, Any, List
5 |
6 | env = environ.Env(
7 | FIRE_CRAWL_API_TOKEN=str,
8 | )
9 |
10 | class FirecrawlWebSearch:
11 | def __init__(self):
12 | super().__init__()
13 | self.api_key = env("FIRE_CRAWL_API_TOKEN")
14 | self.base_url = "https://api.firecrawl.dev/v1"
15 |
16 | def get_name(self) -> str:
17 | return "web_search"
18 |
19 | def get_description(self) -> str:
20 | return "Search the web and optionally scrape content from search results. Returns titles, descriptions, URLs and optional content."
21 |
22 | def get_parameters(self) -> Dict[str, Any]:
23 | return {
24 | "type": "object",
25 | "properties": {
26 | "query": {
27 | "type": "string",
28 | "description": "The search query"
29 | },
30 | "limit": {
31 | "type": "integer",
32 | "description": "Number of search results to return",
33 | "default": 5,
34 | "minimum": 1,
35 | "maximum": 20
36 | },
37 | "location": {
38 | "type": "string",
39 | "description": "Location for localized search results (e.g., 'Germany', 'United States')"
40 | },
41 | "tbs": {
42 | "type": "string",
43 | "description": "Time-based search filter",
44 | "enum": ["qdr:h", "qdr:d", "qdr:w", "qdr:m", "qdr:y"]
45 | },
46 | "scrape_content": {
47 | "type": "boolean",
48 | "description": "Whether to scrape content from search results",
49 | "default": False
50 | },
51 | "formats": {
52 | "type": "array",
53 | "items": {
54 | "type": "string",
55 | "enum": ["markdown", "html", "rawHtml", "links", "screenshot"]
56 | },
57 | "description": "Content formats to scrape (only used if scrape_content is true)",
58 | "default": ["markdown"]
59 | }
60 | },
61 | "required": ["query"]
62 | }
63 |
64 | async def execute(self, **kwargs) -> Dict[str, Any]:
65 | """Execute Firecrawl web search"""
66 | if not self.api_key:
67 | return {
68 | "error": "Firecrawl API key not configured",
69 | "results": []
70 | }
71 |
72 | # Prepare request payload
73 | payload = {
74 | "query": kwargs["query"],
75 | "limit": kwargs.get("limit", 5)
76 | }
77 |
78 | # Add optional parameters
79 | if "location" in kwargs:
80 | payload["location"] = kwargs["location"]
81 | if "tbs" in kwargs:
82 | payload["tbs"] = kwargs["tbs"]
83 |
84 | # Add scraping options if requested
85 | if kwargs.get("scrape_content", False):
86 | payload["scrapeOptions"] = {
87 | "formats": kwargs.get("formats", ["markdown"])
88 | }
89 |
90 | try:
91 | response = requests.post(
92 | f"{self.base_url}/search",
93 | headers={
94 | "Content-Type": "application/json",
95 | "Authorization": f"Bearer {self.api_key}"
96 | },
97 | json=payload,
98 | timeout=30
99 | )
100 |
101 | if response.status_code == 200:
102 | data = response.json()
103 | return {
104 | "success": True,
105 | "results": data.get("data", []),
106 | "query": kwargs["query"]
107 | }
108 | else:
109 | print("firecrawl:search failed with status", f"Bearer {self.api_key}")
110 | return {
111 | "error": f"Search failed with status {response.status_code}",
112 | "details": response.text,
113 | "results": []
114 | }
115 |
116 | except requests.RequestException as e:
117 | return {
118 | "error": f"Network error: {str(e)}",
119 | "results": []
120 | }
121 | except Exception as e:
122 | return {
123 | "error": f"Unexpected error: {str(e)}",
124 | "results": []
125 | }
126 |
--------------------------------------------------------------------------------
/ui/src/components/ui/prompt-input.tsx:
--------------------------------------------------------------------------------
1 | import { Textarea } from "@/components/ui/textarea"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip"
8 | import { cn } from "@/lib/utils"
9 | import React, {
10 | createContext,
11 | useContext,
12 | useEffect,
13 | useRef,
14 | useState,
15 | } from "react"
16 |
17 | type PromptInputContextType = {
18 | isLoading: boolean
19 | value: string
20 | setValue: (value: string) => void
21 | maxHeight: number | string
22 | onSubmit?: () => void
23 | disabled?: boolean
24 | }
25 |
26 | const PromptInputContext = createContext({
27 | isLoading: false,
28 | value: "",
29 | setValue: () => {},
30 | maxHeight: 240,
31 | onSubmit: undefined,
32 | disabled: false,
33 | })
34 |
35 | function usePromptInput() {
36 | const context = useContext(PromptInputContext)
37 | if (!context) {
38 | throw new Error("usePromptInput must be used within a PromptInput")
39 | }
40 | return context
41 | }
42 |
43 | type PromptInputProps = {
44 | isLoading?: boolean
45 | value?: string
46 | onValueChange?: (value: string) => void
47 | maxHeight?: number | string
48 | onSubmit?: () => void
49 | children: React.ReactNode
50 | className?: string
51 | }
52 |
53 | function PromptInput({
54 | className,
55 | isLoading = false,
56 | maxHeight = 240,
57 | value,
58 | onValueChange,
59 | onSubmit,
60 | children,
61 | }: PromptInputProps) {
62 | const [internalValue, setInternalValue] = useState(value || "")
63 |
64 | const handleChange = (newValue: string) => {
65 | setInternalValue(newValue)
66 | onValueChange?.(newValue)
67 | }
68 |
69 | return (
70 |
71 |
80 |
86 | {children}
87 |
88 |
89 |
90 | )
91 | }
92 |
93 | export type PromptInputTextareaProps = {
94 | disableAutosize?: boolean
95 | } & React.ComponentProps
96 |
97 | function PromptInputTextarea({
98 | className,
99 | onKeyDown,
100 | disableAutosize = false,
101 | ...props
102 | }: PromptInputTextareaProps) {
103 | const { value, setValue, maxHeight, onSubmit, disabled } = usePromptInput()
104 | const textareaRef = useRef(null)
105 |
106 | useEffect(() => {
107 | if (disableAutosize) return
108 |
109 | if (!textareaRef.current) return
110 | textareaRef.current.style.height = "auto"
111 | textareaRef.current.style.height =
112 | typeof maxHeight === "number"
113 | ? `${Math.min(textareaRef.current.scrollHeight, maxHeight)}px`
114 | : `min(${textareaRef.current.scrollHeight}px, ${maxHeight})`
115 | }, [value, maxHeight, disableAutosize])
116 |
117 | const handleKeyDown = (e: React.KeyboardEvent) => {
118 | if (e.key === "Enter" && !e.shiftKey) {
119 | e.preventDefault()
120 | onSubmit?.()
121 | }
122 | onKeyDown?.(e)
123 | }
124 |
125 | return (
126 |