Merge branch 'main' into dev-docker-compose

This commit is contained in:
Edwin Jose 2025-09-18 16:51:59 -04:00
commit 1736ff56bd
16 changed files with 1095 additions and 1278 deletions

View file

@ -62,8 +62,7 @@ LANGFLOW_CHAT_FLOW_ID=your_chat_flow_id
LANGFLOW_INGEST_FLOW_ID=your_ingest_flow_id LANGFLOW_INGEST_FLOW_ID=your_ingest_flow_id
NUDGES_FLOW_ID=your_nudges_flow_id NUDGES_FLOW_ID=your_nudges_flow_id
``` ```
ee extended configuration, including ingestion and optional variables: [docs/configuration.md](docs/ See extended configuration, including ingestion and optional variables: [docs/configuration.md](docs/configuration.md)
configuration.md)
### 3. Start OpenRAG ### 3. Start OpenRAG
```bash ```bash

View file

@ -91,7 +91,7 @@ services:
langflow: langflow:
volumes: volumes:
- ./flows:/app/flows:Z - ./flows:/app/flows:Z
image: phact/langflow:${LANGFLOW_VERSION:-responses} image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest}
container_name: langflow container_name: langflow
ports: ports:
- "7860:7860" - "7860:7860"

View file

@ -1,12 +1,12 @@
"use client"; "use client";
import { EllipsisVertical } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { EllipsisVertical } from "lucide-react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
export function KnowledgeActionsDropdown() { export function KnowledgeActionsDropdown() {
@ -18,7 +18,9 @@ export function KnowledgeActionsDropdown() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="right" sideOffset={-10}> <DropdownMenuContent side="right" sideOffset={-10}>
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> <DropdownMenuItem className="text-destructive focus:text-destructive-foreground focus:bg-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View file

@ -62,7 +62,7 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
<Markdown <Markdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeMathjax, rehypeRaw]} rehypePlugins={[rehypeMathjax, rehypeRaw]}
linkTarget="_blank" urlTransform={(url) => url}
components={{ components={{
p({ node, ...props }) { p({ node, ...props }) {
return <p className="w-fit max-w-full">{props.children}</p>; return <p className="w-fit max-w-full">{props.children}</p>;
@ -79,7 +79,7 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
h3({ node, ...props }) { h3({ node, ...props }) {
return <h3 className="mb-2 mt-4">{props.children}</h3>; return <h3 className="mb-2 mt-4">{props.children}</h3>;
}, },
hr({ node, ...props }) { hr() {
return <hr className="w-full mt-4 mb-8" />; return <hr className="w-full mt-4 mb-8" />;
}, },
ul({ node, ...props }) { ul({ node, ...props }) {
@ -97,8 +97,12 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
</div> </div>
); );
}, },
a({ node, ...props }) {
return <a {...props} target="_blank" rel="noopener noreferrer">{props.children}</a>;
},
code: ({ node, className, inline, children, ...props }) => { code(props) {
const { children, className, ...rest } = props;
let content = children as string; let content = children as string;
if ( if (
Array.isArray(children) && Array.isArray(children) &&
@ -120,14 +124,15 @@ export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => {
} }
const match = /language-(\w+)/.exec(className || ""); const match = /language-(\w+)/.exec(className || "");
const isInline = !className?.startsWith("language-");
return !inline ? ( return !isInline ? (
<CodeComponent <CodeComponent
language={(match && match[1]) || ""} language={(match && match[1]) || ""}
code={String(content).replace(/\n$/, "")} code={String(content).replace(/\n$/, "")}
/> />
) : ( ) : (
<code className={className} {...props}> <code className={className} {...rest}>
{content} {content}
</code> </code>
); );

File diff suppressed because it is too large Load diff

View file

@ -38,11 +38,11 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^8.0.7", "react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"rehype-mathjax": "^4.0.3", "rehype-mathjax": "^7.1.0",
"rehype-raw": "^6.1.1", "rehype-raw": "^7.0.0",
"remark-gfm": "3.0.1", "remark-gfm": "^4.0.1",
"sonner": "^2.0.6", "sonner": "^2.0.6",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@ -53,6 +53,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.5", "eslint-config-next": "15.3.5",

View file

@ -18,8 +18,7 @@ async def chat_endpoint(request: Request, chat_service, session_manager):
user = request.state.user user = request.state.user
user_id = user.user_id user_id = user.user_id
# Get JWT token from auth middleware jwt_token = session_manager.get_effective_jwt_token(user_id, request.state.jwt_token)
jwt_token = request.state.jwt_token
if not prompt: if not prompt:
return JSONResponse({"error": "Prompt is required"}, status_code=400) return JSONResponse({"error": "Prompt is required"}, status_code=400)
@ -76,8 +75,7 @@ async def langflow_endpoint(request: Request, chat_service, session_manager):
user = request.state.user user = request.state.user
user_id = user.user_id user_id = user.user_id
# Get JWT token from auth middleware jwt_token = session_manager.get_effective_jwt_token(user_id, request.state.jwt_token)
jwt_token = request.state.jwt_token
if not prompt: if not prompt:
return JSONResponse({"error": "Prompt is required"}, status_code=400) return JSONResponse({"error": "Prompt is required"}, status_code=400)

View file

@ -13,8 +13,8 @@ async def list_connectors(request: Request, connector_service, session_manager):
) )
return JSONResponse({"connectors": connector_types}) return JSONResponse({"connectors": connector_types})
except Exception as e: except Exception as e:
logger.error("Error listing connectors", error=str(e)) logger.info("Error listing connectors", error=str(e))
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"connectors": []})
async def connector_sync(request: Request, connector_service, session_manager): async def connector_sync(request: Request, connector_service, session_manager):
@ -31,7 +31,7 @@ async def connector_sync(request: Request, connector_service, session_manager):
max_files=max_files, max_files=max_files,
) )
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
# Get all active connections for this connector type and user # Get all active connections for this connector type and user
connections = await connector_service.connection_manager.list_connections( connections = await connector_service.connection_manager.list_connections(

View file

@ -26,7 +26,7 @@ async def create_knowledge_filter(
return JSONResponse({"error": "Query data is required"}, status_code=400) return JSONResponse({"error": "Query data is required"}, status_code=400)
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
# Create knowledge filter document # Create knowledge filter document
filter_id = str(uuid.uuid4()) filter_id = str(uuid.uuid4())
@ -70,7 +70,7 @@ async def search_knowledge_filters(
limit = payload.get("limit", 20) limit = payload.get("limit", 20)
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
result = await knowledge_filter_service.search_knowledge_filters( result = await knowledge_filter_service.search_knowledge_filters(
query, user_id=user.user_id, jwt_token=jwt_token, limit=limit query, user_id=user.user_id, jwt_token=jwt_token, limit=limit
@ -101,7 +101,7 @@ async def get_knowledge_filter(
) )
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
result = await knowledge_filter_service.get_knowledge_filter( result = await knowledge_filter_service.get_knowledge_filter(
filter_id, user_id=user.user_id, jwt_token=jwt_token filter_id, user_id=user.user_id, jwt_token=jwt_token
@ -136,7 +136,7 @@ async def update_knowledge_filter(
payload = await request.json() payload = await request.json()
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
# First, get the existing knowledge filter # First, get the existing knowledge filter
existing_result = await knowledge_filter_service.get_knowledge_filter( existing_result = await knowledge_filter_service.get_knowledge_filter(
@ -205,7 +205,7 @@ async def delete_knowledge_filter(
) )
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
result = await knowledge_filter_service.delete_knowledge_filter( result = await knowledge_filter_service.delete_knowledge_filter(
filter_id, user_id=user.user_id, jwt_token=jwt_token filter_id, user_id=user.user_id, jwt_token=jwt_token
@ -239,7 +239,7 @@ async def subscribe_to_knowledge_filter(
payload = await request.json() payload = await request.json()
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
# Get the knowledge filter to validate it exists and get its details # Get the knowledge filter to validate it exists and get its details
filter_result = await knowledge_filter_service.get_knowledge_filter( filter_result = await knowledge_filter_service.get_knowledge_filter(
@ -309,7 +309,7 @@ async def list_knowledge_filter_subscriptions(
) )
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
result = await knowledge_filter_service.get_filter_subscriptions( result = await knowledge_filter_service.get_filter_subscriptions(
filter_id, user_id=user.user_id, jwt_token=jwt_token filter_id, user_id=user.user_id, jwt_token=jwt_token
@ -341,7 +341,7 @@ async def cancel_knowledge_filter_subscription(
) )
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
# Get subscription details to find the monitor ID # Get subscription details to find the monitor ID
subscriptions_result = await knowledge_filter_service.get_filter_subscriptions( subscriptions_result = await knowledge_filter_service.get_filter_subscriptions(

View file

@ -9,7 +9,7 @@ async def nudges_from_kb_endpoint(request: Request, chat_service, session_manage
"""Get nudges for a user""" """Get nudges for a user"""
user = request.state.user user = request.state.user
user_id = user.user_id user_id = user.user_id
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user_id, request.state.jwt_token)
try: try:
result = await chat_service.langflow_nudges_chat( result = await chat_service.langflow_nudges_chat(
@ -28,7 +28,8 @@ async def nudges_from_chat_id_endpoint(request: Request, chat_service, session_m
user = request.state.user user = request.state.user
user_id = user.user_id user_id = user.user_id
chat_id = request.path_params["chat_id"] chat_id = request.path_params["chat_id"]
jwt_token = request.state.jwt_token
jwt_token = session_manager.get_effective_jwt_token(user_id, request.state.jwt_token)
try: try:
result = await chat_service.langflow_nudges_chat( result = await chat_service.langflow_nudges_chat(

View file

@ -20,8 +20,7 @@ async def search(request: Request, search_service, session_manager):
) # Optional score threshold, defaults to 0 ) # Optional score threshold, defaults to 0
user = request.state.user user = request.state.user
# Extract JWT token from auth middleware jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
jwt_token = request.state.jwt_token
logger.debug( logger.debug(
"Search API request", "Search API request",

View file

@ -11,7 +11,7 @@ async def upload(request: Request, document_service, session_manager):
form = await request.form() form = await request.form()
upload_file = form["file"] upload_file = form["file"]
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
from config.settings import is_no_auth_mode from config.settings import is_no_auth_mode
@ -60,7 +60,7 @@ async def upload_path(request: Request, task_service, session_manager):
return JSONResponse({"error": "No files found in directory"}, status_code=400) return JSONResponse({"error": "No files found in directory"}, status_code=400)
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
from config.settings import is_no_auth_mode from config.settings import is_no_auth_mode
@ -100,8 +100,7 @@ async def upload_context(
previous_response_id = form.get("previous_response_id") previous_response_id = form.get("previous_response_id")
endpoint = form.get("endpoint", "langflow") endpoint = form.get("endpoint", "langflow")
# Get JWT token from auth middleware jwt_token = session_manager.get_effective_jwt_token(user_id, request.state.jwt_token)
jwt_token = request.state.jwt_token
# Get user info from request state (set by auth middleware) # Get user info from request state (set by auth middleware)
user = request.state.user user = request.state.user
@ -169,7 +168,7 @@ async def upload_bucket(request: Request, task_service, session_manager):
return JSONResponse({"error": "No files found in bucket"}, status_code=400) return JSONResponse({"error": "No files found in bucket"}, status_code=400)
user = request.state.user user = request.state.user
jwt_token = request.state.jwt_token jwt_token = session_manager.get_effective_jwt_token(user.user_id, request.state.jwt_token)
from models.processors import S3FileProcessor from models.processors import S3FileProcessor
from config.settings import is_no_auth_mode from config.settings import is_no_auth_mode

View file

@ -321,7 +321,7 @@ class ConnectionManager:
return None return None
def get_available_connector_types(self) -> Dict[str, Dict[str, str]]: def get_available_connector_types(self) -> Dict[str, Dict[str, Any]]:
"""Get available connector types with their metadata""" """Get available connector types with their metadata"""
return { return {
"google_drive": { "google_drive": {

View file

@ -191,26 +191,8 @@ class SessionManager:
def get_user_opensearch_client(self, user_id: str, jwt_token: str): def get_user_opensearch_client(self, user_id: str, jwt_token: str):
"""Get or create OpenSearch client for user with their JWT""" """Get or create OpenSearch client for user with their JWT"""
from config.settings import is_no_auth_mode # Get the effective JWT token (handles anonymous JWT creation)
jwt_token = self.get_effective_jwt_token(user_id, jwt_token)
logger.debug(
"get_user_opensearch_client",
user_id=user_id,
jwt_token_present=(jwt_token is not None),
no_auth_mode=is_no_auth_mode(),
)
# In no-auth mode, create anonymous JWT for OpenSearch DLS
if jwt_token is None and (is_no_auth_mode() or user_id in (None, AnonymousUser().user_id)):
if not hasattr(self, "_anonymous_jwt"):
# Create anonymous JWT token for OpenSearch OIDC
logger.debug("Creating anonymous JWT")
self._anonymous_jwt = self._create_anonymous_jwt()
logger.debug(
"Anonymous JWT created", jwt_prefix=self._anonymous_jwt[:50]
)
jwt_token = self._anonymous_jwt
logger.debug("Using anonymous JWT for OpenSearch")
# Check if we have a cached client for this user # Check if we have a cached client for this user
if user_id not in self.user_opensearch_clients: if user_id not in self.user_opensearch_clients:
@ -222,6 +204,31 @@ class SessionManager:
return self.user_opensearch_clients[user_id] return self.user_opensearch_clients[user_id]
def get_effective_jwt_token(self, user_id: str, jwt_token: str) -> str:
"""Get the effective JWT token, creating anonymous JWT if needed in no-auth mode"""
from config.settings import is_no_auth_mode
logger.debug(
"get_effective_jwt_token",
user_id=user_id,
jwt_token_present=(jwt_token is not None),
no_auth_mode=is_no_auth_mode(),
)
# In no-auth mode, create anonymous JWT if needed
if jwt_token is None and (is_no_auth_mode() or user_id in (None, AnonymousUser().user_id)):
if not hasattr(self, "_anonymous_jwt"):
# Create anonymous JWT token for OpenSearch OIDC
logger.debug("Creating anonymous JWT")
self._anonymous_jwt = self._create_anonymous_jwt()
logger.debug(
"Anonymous JWT created", jwt_prefix=self._anonymous_jwt[:50]
)
jwt_token = self._anonymous_jwt
logger.debug("Using anonymous JWT")
return jwt_token
def _create_anonymous_jwt(self) -> str: def _create_anonymous_jwt(self) -> str:
"""Create JWT token for anonymous user in no-auth mode""" """Create JWT token for anonymous user in no-auth mode"""
anonymous_user = AnonymousUser() anonymous_user = AnonymousUser()

View file

@ -91,7 +91,7 @@ services:
langflow: langflow:
volumes: volumes:
- ./flows:/app/flows:Z - ./flows:/app/flows:Z
image: phact/langflow:${LANGFLOW_VERSION:-responses} image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest}
container_name: langflow container_name: langflow
ports: ports:
- "7860:7860" - "7860:7860"

View file

@ -91,7 +91,7 @@ services:
langflow: langflow:
volumes: volumes:
- ./flows:/app/flows:Z - ./flows:/app/flows:Z
image: phact/langflow:${LANGFLOW_VERSION:-responses} image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest}
container_name: langflow container_name: langflow
ports: ports:
- "7860:7860" - "7860:7860"