Add nudges service into backend
This commit is contained in:
parent
c87877bb80
commit
aad89b9166
6 changed files with 639 additions and 240 deletions
163
src/agent.py
163
src/agent.py
|
|
@ -95,7 +95,9 @@ async def async_response_stream(
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
async for chunk in response:
|
async for chunk in response:
|
||||||
chunk_count += 1
|
chunk_count += 1
|
||||||
logger.debug("Stream chunk received", chunk_count=chunk_count, chunk=str(chunk))
|
logger.debug(
|
||||||
|
"Stream chunk received", chunk_count=chunk_count, chunk=str(chunk)
|
||||||
|
)
|
||||||
|
|
||||||
# Yield the raw event as JSON for the UI to process
|
# Yield the raw event as JSON for the UI to process
|
||||||
import json
|
import json
|
||||||
|
|
@ -171,6 +173,10 @@ async def async_response(
|
||||||
if extra_headers:
|
if extra_headers:
|
||||||
request_params["extra_headers"] = extra_headers
|
request_params["extra_headers"] = extra_headers
|
||||||
|
|
||||||
|
if "x-api-key" not in client.default_headers:
|
||||||
|
if hasattr(client, "api_key") and extra_headers is not None:
|
||||||
|
extra_headers["x-api-key"] = client.api_key
|
||||||
|
|
||||||
response = await client.responses.create(**request_params)
|
response = await client.responses.create(**request_params)
|
||||||
|
|
||||||
response_text = response.output_text
|
response_text = response.output_text
|
||||||
|
|
@ -241,7 +247,10 @@ async def async_langflow_stream(
|
||||||
previous_response_id=previous_response_id,
|
previous_response_id=previous_response_id,
|
||||||
log_prefix="langflow",
|
log_prefix="langflow",
|
||||||
):
|
):
|
||||||
logger.debug("Yielding chunk from langflow stream", chunk_preview=chunk[:100].decode('utf-8', errors='replace'))
|
logger.debug(
|
||||||
|
"Yielding chunk from langflow stream",
|
||||||
|
chunk_preview=chunk[:100].decode("utf-8", errors="replace"),
|
||||||
|
)
|
||||||
yield chunk
|
yield chunk
|
||||||
logger.debug("Langflow stream completed")
|
logger.debug("Langflow stream completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -260,18 +269,24 @@ async def async_chat(
|
||||||
model: str = "gpt-4.1-mini",
|
model: str = "gpt-4.1-mini",
|
||||||
previous_response_id: str = None,
|
previous_response_id: str = None,
|
||||||
):
|
):
|
||||||
logger.debug("async_chat called", user_id=user_id, previous_response_id=previous_response_id)
|
logger.debug(
|
||||||
|
"async_chat called", user_id=user_id, previous_response_id=previous_response_id
|
||||||
|
)
|
||||||
|
|
||||||
# Get the specific conversation thread (or create new one)
|
# Get the specific conversation thread (or create new one)
|
||||||
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
||||||
logger.debug("Got conversation state", message_count=len(conversation_state['messages']))
|
logger.debug(
|
||||||
|
"Got conversation state", message_count=len(conversation_state["messages"])
|
||||||
|
)
|
||||||
|
|
||||||
# Add user message to conversation with timestamp
|
# Add user message to conversation with timestamp
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
|
user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
|
||||||
conversation_state["messages"].append(user_message)
|
conversation_state["messages"].append(user_message)
|
||||||
logger.debug("Added user message", message_count=len(conversation_state['messages']))
|
logger.debug(
|
||||||
|
"Added user message", message_count=len(conversation_state["messages"])
|
||||||
|
)
|
||||||
|
|
||||||
response_text, response_id = await async_response(
|
response_text, response_id = await async_response(
|
||||||
async_client,
|
async_client,
|
||||||
|
|
@ -280,7 +295,9 @@ async def async_chat(
|
||||||
previous_response_id=previous_response_id,
|
previous_response_id=previous_response_id,
|
||||||
log_prefix="agent",
|
log_prefix="agent",
|
||||||
)
|
)
|
||||||
logger.debug("Got response", response_preview=response_text[:50], response_id=response_id)
|
logger.debug(
|
||||||
|
"Got response", response_preview=response_text[:50], response_id=response_id
|
||||||
|
)
|
||||||
|
|
||||||
# Add assistant response to conversation with response_id and timestamp
|
# Add assistant response to conversation with response_id and timestamp
|
||||||
assistant_message = {
|
assistant_message = {
|
||||||
|
|
@ -290,17 +307,26 @@ async def async_chat(
|
||||||
"timestamp": datetime.now(),
|
"timestamp": datetime.now(),
|
||||||
}
|
}
|
||||||
conversation_state["messages"].append(assistant_message)
|
conversation_state["messages"].append(assistant_message)
|
||||||
logger.debug("Added assistant message", message_count=len(conversation_state['messages']))
|
logger.debug(
|
||||||
|
"Added assistant message", message_count=len(conversation_state["messages"])
|
||||||
|
)
|
||||||
|
|
||||||
# Store the conversation thread with its response_id
|
# Store the conversation thread with its response_id
|
||||||
if response_id:
|
if response_id:
|
||||||
conversation_state["last_activity"] = datetime.now()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
store_conversation_thread(user_id, response_id, conversation_state)
|
||||||
logger.debug("Stored conversation thread", user_id=user_id, response_id=response_id)
|
logger.debug(
|
||||||
|
"Stored conversation thread", user_id=user_id, response_id=response_id
|
||||||
|
)
|
||||||
|
|
||||||
# Debug: Check what's in user_conversations now
|
# Debug: Check what's in user_conversations now
|
||||||
conversations = get_user_conversations(user_id)
|
conversations = get_user_conversations(user_id)
|
||||||
logger.debug("User conversations updated", user_id=user_id, conversation_count=len(conversations), conversation_ids=list(conversations.keys()))
|
logger.debug(
|
||||||
|
"User conversations updated",
|
||||||
|
user_id=user_id,
|
||||||
|
conversation_count=len(conversations),
|
||||||
|
conversation_ids=list(conversations.keys()),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("No response_id received, conversation not stored")
|
logger.warning("No response_id received, conversation not stored")
|
||||||
|
|
||||||
|
|
@ -363,7 +389,9 @@ async def async_chat_stream(
|
||||||
if response_id:
|
if response_id:
|
||||||
conversation_state["last_activity"] = datetime.now()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
store_conversation_thread(user_id, response_id, conversation_state)
|
||||||
logger.debug("Stored conversation thread", user_id=user_id, response_id=response_id)
|
logger.debug(
|
||||||
|
"Stored conversation thread", user_id=user_id, response_id=response_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Async langflow function with conversation storage (non-streaming)
|
# Async langflow function with conversation storage (non-streaming)
|
||||||
|
|
@ -374,19 +402,32 @@ async def async_langflow_chat(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
extra_headers: dict = None,
|
extra_headers: dict = None,
|
||||||
previous_response_id: str = None,
|
previous_response_id: str = None,
|
||||||
|
store_conversation: bool = True,
|
||||||
):
|
):
|
||||||
logger.debug("async_langflow_chat called", user_id=user_id, previous_response_id=previous_response_id)
|
logger.debug(
|
||||||
|
"async_langflow_chat called",
|
||||||
|
user_id=user_id,
|
||||||
|
previous_response_id=previous_response_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Get the specific conversation thread (or create new one)
|
if store_conversation:
|
||||||
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
# Get the specific conversation thread (or create new one)
|
||||||
logger.debug("Got langflow conversation state", message_count=len(conversation_state['messages']))
|
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
||||||
|
logger.debug(
|
||||||
|
"Got langflow conversation state",
|
||||||
|
message_count=len(conversation_state["messages"]),
|
||||||
|
)
|
||||||
|
|
||||||
# Add user message to conversation with timestamp
|
# Add user message to conversation with timestamp
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
|
if store_conversation:
|
||||||
conversation_state["messages"].append(user_message)
|
user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
|
||||||
logger.debug("Added user message to langflow", message_count=len(conversation_state['messages']))
|
conversation_state["messages"].append(user_message)
|
||||||
|
logger.debug(
|
||||||
|
"Added user message to langflow",
|
||||||
|
message_count=len(conversation_state["messages"]),
|
||||||
|
)
|
||||||
|
|
||||||
response_text, response_id = await async_response(
|
response_text, response_id = await async_response(
|
||||||
langflow_client,
|
langflow_client,
|
||||||
|
|
@ -396,45 +437,69 @@ async def async_langflow_chat(
|
||||||
previous_response_id=previous_response_id,
|
previous_response_id=previous_response_id,
|
||||||
log_prefix="langflow",
|
log_prefix="langflow",
|
||||||
)
|
)
|
||||||
logger.debug("Got langflow response", response_preview=response_text[:50], response_id=response_id)
|
logger.debug(
|
||||||
|
"Got langflow response",
|
||||||
|
response_preview=response_text[:50],
|
||||||
|
response_id=response_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Add assistant response to conversation with response_id and timestamp
|
if store_conversation:
|
||||||
assistant_message = {
|
# Add assistant response to conversation with response_id and timestamp
|
||||||
"role": "assistant",
|
assistant_message = {
|
||||||
"content": response_text,
|
"role": "assistant",
|
||||||
"response_id": response_id,
|
"content": response_text,
|
||||||
"timestamp": datetime.now(),
|
"response_id": response_id,
|
||||||
}
|
"timestamp": datetime.now(),
|
||||||
conversation_state["messages"].append(assistant_message)
|
}
|
||||||
logger.debug("Added assistant message to langflow", message_count=len(conversation_state['messages']))
|
conversation_state["messages"].append(assistant_message)
|
||||||
|
logger.debug(
|
||||||
|
"Added assistant message to langflow",
|
||||||
|
message_count=len(conversation_state["messages"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not store_conversation:
|
||||||
|
return response_text, response_id
|
||||||
|
|
||||||
# Store the conversation thread with its response_id
|
# Store the conversation thread with its response_id
|
||||||
if response_id:
|
if response_id:
|
||||||
conversation_state["last_activity"] = datetime.now()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
store_conversation_thread(user_id, response_id, conversation_state)
|
||||||
|
|
||||||
# Claim session ownership if this is a Google user
|
# Claim session ownership if this is a Google user
|
||||||
try:
|
try:
|
||||||
from services.session_ownership_service import session_ownership_service
|
from services.session_ownership_service import session_ownership_service
|
||||||
from services.user_binding_service import user_binding_service
|
from services.user_binding_service import user_binding_service
|
||||||
|
|
||||||
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
||||||
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
||||||
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
||||||
if langflow_user_id:
|
if langflow_user_id:
|
||||||
session_ownership_service.claim_session(user_id, response_id, langflow_user_id)
|
session_ownership_service.claim_session(
|
||||||
print(f"[DEBUG] Claimed session {response_id} for Google user {user_id}")
|
user_id, response_id, langflow_user_id
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[DEBUG] Claimed session {response_id} for Google user {user_id}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WARNING] Failed to claim session ownership: {e}")
|
print(f"[WARNING] Failed to claim session ownership: {e}")
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
||||||
)
|
)
|
||||||
logger.debug("Stored langflow conversation thread", user_id=user_id, response_id=response_id)
|
logger.debug(
|
||||||
|
"Stored langflow conversation thread",
|
||||||
|
user_id=user_id,
|
||||||
|
response_id=response_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Debug: Check what's in user_conversations now
|
# Debug: Check what's in user_conversations now
|
||||||
conversations = get_user_conversations(user_id)
|
conversations = get_user_conversations(user_id)
|
||||||
logger.debug("User conversations updated", user_id=user_id, conversation_count=len(conversations), conversation_ids=list(conversations.keys()))
|
logger.debug(
|
||||||
|
"User conversations updated",
|
||||||
|
user_id=user_id,
|
||||||
|
conversation_count=len(conversations),
|
||||||
|
conversation_ids=list(conversations.keys()),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("No response_id received from langflow, conversation not stored")
|
logger.warning("No response_id received from langflow, conversation not stored")
|
||||||
|
|
||||||
|
|
@ -450,7 +515,11 @@ async def async_langflow_chat_stream(
|
||||||
extra_headers: dict = None,
|
extra_headers: dict = None,
|
||||||
previous_response_id: str = None,
|
previous_response_id: str = None,
|
||||||
):
|
):
|
||||||
logger.debug("async_langflow_chat_stream called", user_id=user_id, previous_response_id=previous_response_id)
|
logger.debug(
|
||||||
|
"async_langflow_chat_stream called",
|
||||||
|
user_id=user_id,
|
||||||
|
previous_response_id=previous_response_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Get the specific conversation thread (or create new one)
|
# Get the specific conversation thread (or create new one)
|
||||||
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
conversation_state = get_conversation_thread(user_id, previous_response_id)
|
||||||
|
|
@ -501,22 +570,32 @@ async def async_langflow_chat_stream(
|
||||||
if response_id:
|
if response_id:
|
||||||
conversation_state["last_activity"] = datetime.now()
|
conversation_state["last_activity"] = datetime.now()
|
||||||
store_conversation_thread(user_id, response_id, conversation_state)
|
store_conversation_thread(user_id, response_id, conversation_state)
|
||||||
|
|
||||||
# Claim session ownership if this is a Google user
|
# Claim session ownership if this is a Google user
|
||||||
try:
|
try:
|
||||||
from services.session_ownership_service import session_ownership_service
|
from services.session_ownership_service import session_ownership_service
|
||||||
from services.user_binding_service import user_binding_service
|
from services.user_binding_service import user_binding_service
|
||||||
|
|
||||||
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
# Check if this is a Google user (Google IDs are numeric, Langflow IDs are UUID)
|
||||||
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
if user_id.isdigit() and user_binding_service.has_binding(user_id):
|
||||||
langflow_user_id = user_binding_service.get_langflow_user_id(user_id)
|
langflow_user_id = user_binding_service.get_langflow_user_id(
|
||||||
|
user_id
|
||||||
|
)
|
||||||
if langflow_user_id:
|
if langflow_user_id:
|
||||||
session_ownership_service.claim_session(user_id, response_id, langflow_user_id)
|
session_ownership_service.claim_session(
|
||||||
print(f"[DEBUG] Claimed session {response_id} for Google user {user_id} (streaming)")
|
user_id, response_id, langflow_user_id
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[DEBUG] Claimed session {response_id} for Google user {user_id} (streaming)"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WARNING] Failed to claim session ownership (streaming): {e}")
|
print(f"[WARNING] Failed to claim session ownership (streaming): {e}")
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
f"[DEBUG] Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
|
||||||
)
|
)
|
||||||
logger.debug("Stored langflow conversation thread", user_id=user_id, response_id=response_id)
|
logger.debug(
|
||||||
|
"Stored langflow conversation thread",
|
||||||
|
user_id=user_id,
|
||||||
|
response_id=response_id,
|
||||||
|
)
|
||||||
|
|
|
||||||
43
src/api/nudges.py
Normal file
43
src/api/nudges.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def nudges_from_kb_endpoint(request: Request, chat_service, session_manager):
|
||||||
|
"""Get nudges for a user"""
|
||||||
|
user = request.state.user
|
||||||
|
user_id = user.user_id
|
||||||
|
jwt_token = request.state.jwt_token
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await chat_service.langflow_nudges_chat(
|
||||||
|
user_id,
|
||||||
|
jwt_token,
|
||||||
|
)
|
||||||
|
return JSONResponse(result)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": f"Failed to get nudges: {str(e)}"}, status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def nudges_from_chat_id_endpoint(request: Request, chat_service, session_manager):
|
||||||
|
"""Get nudges for a user"""
|
||||||
|
user = request.state.user
|
||||||
|
user_id = user.user_id
|
||||||
|
chat_id = request.path_params["chat_id"]
|
||||||
|
jwt_token = request.state.jwt_token
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await chat_service.langflow_nudges_chat(
|
||||||
|
user_id,
|
||||||
|
jwt_token,
|
||||||
|
previous_response_id=chat_id,
|
||||||
|
)
|
||||||
|
return JSONResponse(result)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": f"Failed to get nudges: {str(e)}"}, status_code=500
|
||||||
|
)
|
||||||
|
|
@ -24,6 +24,7 @@ LANGFLOW_URL = os.getenv("LANGFLOW_URL", "http://localhost:7860")
|
||||||
# Optional: public URL for browser links (e.g., http://localhost:7860)
|
# Optional: public URL for browser links (e.g., http://localhost:7860)
|
||||||
LANGFLOW_PUBLIC_URL = os.getenv("LANGFLOW_PUBLIC_URL")
|
LANGFLOW_PUBLIC_URL = os.getenv("LANGFLOW_PUBLIC_URL")
|
||||||
FLOW_ID = os.getenv("FLOW_ID")
|
FLOW_ID = os.getenv("FLOW_ID")
|
||||||
|
NUDGES_FLOW_ID = os.getenv("NUDGES_FLOW_ID")
|
||||||
# Langflow superuser credentials for API key generation
|
# Langflow superuser credentials for API key generation
|
||||||
LANGFLOW_SUPERUSER = os.getenv("LANGFLOW_SUPERUSER")
|
LANGFLOW_SUPERUSER = os.getenv("LANGFLOW_SUPERUSER")
|
||||||
LANGFLOW_SUPERUSER_PASSWORD = os.getenv("LANGFLOW_SUPERUSER_PASSWORD")
|
LANGFLOW_SUPERUSER_PASSWORD = os.getenv("LANGFLOW_SUPERUSER_PASSWORD")
|
||||||
|
|
@ -37,7 +38,12 @@ GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
||||||
def is_no_auth_mode():
|
def is_no_auth_mode():
|
||||||
"""Check if we're running in no-auth mode (OAuth credentials missing)"""
|
"""Check if we're running in no-auth mode (OAuth credentials missing)"""
|
||||||
result = not (GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET)
|
result = not (GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET)
|
||||||
logger.debug("Checking auth mode", no_auth_mode=result, has_client_id=GOOGLE_OAUTH_CLIENT_ID is not None, has_client_secret=GOOGLE_OAUTH_CLIENT_SECRET is not None)
|
logger.debug(
|
||||||
|
"Checking auth mode",
|
||||||
|
no_auth_mode=result,
|
||||||
|
has_client_id=GOOGLE_OAUTH_CLIENT_ID is not None,
|
||||||
|
has_client_secret=GOOGLE_OAUTH_CLIENT_SECRET is not None,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,7 +106,9 @@ async def generate_langflow_api_key():
|
||||||
return LANGFLOW_KEY
|
return LANGFLOW_KEY
|
||||||
|
|
||||||
if not LANGFLOW_SUPERUSER or not LANGFLOW_SUPERUSER_PASSWORD:
|
if not LANGFLOW_SUPERUSER or not LANGFLOW_SUPERUSER_PASSWORD:
|
||||||
logger.warning("LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD not set, skipping API key generation")
|
logger.warning(
|
||||||
|
"LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD not set, skipping API key generation"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -142,11 +150,19 @@ async def generate_langflow_api_key():
|
||||||
raise KeyError("api_key")
|
raise KeyError("api_key")
|
||||||
|
|
||||||
LANGFLOW_KEY = api_key
|
LANGFLOW_KEY = api_key
|
||||||
logger.info("Successfully generated Langflow API key", api_key_preview=api_key[:8])
|
logger.info(
|
||||||
|
"Successfully generated Langflow API key",
|
||||||
|
api_key_preview=api_key[:8],
|
||||||
|
)
|
||||||
return api_key
|
return api_key
|
||||||
except (requests.exceptions.RequestException, KeyError) as e:
|
except (requests.exceptions.RequestException, KeyError) as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
logger.warning("Attempt to generate Langflow API key failed", attempt=attempt, max_attempts=max_attempts, error=str(e))
|
logger.warning(
|
||||||
|
"Attempt to generate Langflow API key failed",
|
||||||
|
attempt=attempt,
|
||||||
|
max_attempts=max_attempts,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
if attempt < max_attempts:
|
if attempt < max_attempts:
|
||||||
time.sleep(delay_seconds)
|
time.sleep(delay_seconds)
|
||||||
else:
|
else:
|
||||||
|
|
@ -196,7 +212,9 @@ class AppClients:
|
||||||
logger.warning("Failed to initialize Langflow client", error=str(e))
|
logger.warning("Failed to initialize Langflow client", error=str(e))
|
||||||
self.langflow_client = None
|
self.langflow_client = None
|
||||||
if self.langflow_client is None:
|
if self.langflow_client is None:
|
||||||
logger.warning("No Langflow client initialized yet, will attempt later on first use")
|
logger.warning(
|
||||||
|
"No Langflow client initialized yet, will attempt later on first use"
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize patched OpenAI client
|
# Initialize patched OpenAI client
|
||||||
self.patched_async_client = patch_openai_with_mcp(AsyncOpenAI())
|
self.patched_async_client = patch_openai_with_mcp(AsyncOpenAI())
|
||||||
|
|
@ -219,7 +237,9 @@ class AppClients:
|
||||||
)
|
)
|
||||||
logger.info("Langflow client initialized on-demand")
|
logger.info("Langflow client initialized on-demand")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to initialize Langflow client on-demand", error=str(e))
|
logger.error(
|
||||||
|
"Failed to initialize Langflow client on-demand", error=str(e)
|
||||||
|
)
|
||||||
self.langflow_client = None
|
self.langflow_client = None
|
||||||
return self.langflow_client
|
return self.langflow_client
|
||||||
|
|
||||||
|
|
|
||||||
84
src/main.py
84
src/main.py
|
|
@ -3,11 +3,13 @@ import sys
|
||||||
# Check for TUI flag FIRST, before any heavy imports
|
# Check for TUI flag FIRST, before any heavy imports
|
||||||
if __name__ == "__main__" and len(sys.argv) > 1 and sys.argv[1] == "--tui":
|
if __name__ == "__main__" and len(sys.argv) > 1 and sys.argv[1] == "--tui":
|
||||||
from tui.main import run_tui
|
from tui.main import run_tui
|
||||||
|
|
||||||
run_tui()
|
run_tui()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Configure structured logging early
|
# Configure structured logging early
|
||||||
from utils.logging_config import configure_from_env, get_logger
|
from utils.logging_config import configure_from_env, get_logger
|
||||||
|
|
||||||
configure_from_env()
|
configure_from_env()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -48,6 +50,7 @@ from auth_middleware import require_auth, optional_auth
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
from api import (
|
from api import (
|
||||||
|
nudges,
|
||||||
upload,
|
upload,
|
||||||
search,
|
search,
|
||||||
chat,
|
chat,
|
||||||
|
|
@ -59,7 +62,11 @@ from api import (
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("CUDA device information", cuda_available=torch.cuda.is_available(), cuda_version=torch.version.cuda)
|
logger.info(
|
||||||
|
"CUDA device information",
|
||||||
|
cuda_available=torch.cuda.is_available(),
|
||||||
|
cuda_version=torch.version.cuda,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_opensearch():
|
async def wait_for_opensearch():
|
||||||
|
|
@ -73,7 +80,12 @@ async def wait_for_opensearch():
|
||||||
logger.info("OpenSearch is ready")
|
logger.info("OpenSearch is ready")
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("OpenSearch not ready yet", attempt=attempt + 1, max_retries=max_retries, error=str(e))
|
logger.warning(
|
||||||
|
"OpenSearch not ready yet",
|
||||||
|
attempt=attempt + 1,
|
||||||
|
max_retries=max_retries,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
await asyncio.sleep(retry_delay)
|
await asyncio.sleep(retry_delay)
|
||||||
else:
|
else:
|
||||||
|
|
@ -95,7 +107,9 @@ async def configure_alerting_security():
|
||||||
|
|
||||||
# Use admin client (clients.opensearch uses admin credentials)
|
# Use admin client (clients.opensearch uses admin credentials)
|
||||||
response = await clients.opensearch.cluster.put_settings(body=alerting_settings)
|
response = await clients.opensearch.cluster.put_settings(body=alerting_settings)
|
||||||
logger.info("Alerting security settings configured successfully", response=response)
|
logger.info(
|
||||||
|
"Alerting security settings configured successfully", response=response
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to configure alerting security settings", error=str(e))
|
logger.warning("Failed to configure alerting security settings", error=str(e))
|
||||||
# Don't fail startup if alerting config fails
|
# Don't fail startup if alerting config fails
|
||||||
|
|
@ -135,9 +149,14 @@ async def init_index():
|
||||||
await clients.opensearch.indices.create(
|
await clients.opensearch.indices.create(
|
||||||
index=knowledge_filter_index_name, body=knowledge_filter_index_body
|
index=knowledge_filter_index_name, body=knowledge_filter_index_body
|
||||||
)
|
)
|
||||||
logger.info("Created knowledge filters index", index_name=knowledge_filter_index_name)
|
logger.info(
|
||||||
|
"Created knowledge filters index", index_name=knowledge_filter_index_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Knowledge filters index already exists, skipping creation", index_name=knowledge_filter_index_name)
|
logger.info(
|
||||||
|
"Knowledge filters index already exists, skipping creation",
|
||||||
|
index_name=knowledge_filter_index_name,
|
||||||
|
)
|
||||||
|
|
||||||
# Configure alerting plugin security settings
|
# Configure alerting plugin security settings
|
||||||
await configure_alerting_security()
|
await configure_alerting_security()
|
||||||
|
|
@ -192,7 +211,9 @@ async def init_index_when_ready():
|
||||||
logger.info("OpenSearch index initialization completed successfully")
|
logger.info("OpenSearch index initialization completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("OpenSearch index initialization failed", error=str(e))
|
logger.error("OpenSearch index initialization failed", error=str(e))
|
||||||
logger.warning("OIDC endpoints will still work, but document operations may fail until OpenSearch is ready")
|
logger.warning(
|
||||||
|
"OIDC endpoints will still work, but document operations may fail until OpenSearch is ready"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def initialize_services():
|
async def initialize_services():
|
||||||
|
|
@ -239,9 +260,14 @@ async def initialize_services():
|
||||||
try:
|
try:
|
||||||
await connector_service.initialize()
|
await connector_service.initialize()
|
||||||
loaded_count = len(connector_service.connection_manager.connections)
|
loaded_count = len(connector_service.connection_manager.connections)
|
||||||
logger.info("Loaded persisted connector connections on startup", loaded_count=loaded_count)
|
logger.info(
|
||||||
|
"Loaded persisted connector connections on startup",
|
||||||
|
loaded_count=loaded_count,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to load persisted connections on startup", error=str(e))
|
logger.warning(
|
||||||
|
"Failed to load persisted connections on startup", error=str(e)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Skipping connector loading in no-auth mode")
|
logger.info("Skipping connector loading in no-auth mode")
|
||||||
|
|
||||||
|
|
@ -626,6 +652,28 @@ async def create_app():
|
||||||
),
|
),
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
),
|
),
|
||||||
|
Route(
|
||||||
|
"/nudges",
|
||||||
|
require_auth(services["session_manager"])(
|
||||||
|
partial(
|
||||||
|
nudges.nudges_from_kb_endpoint,
|
||||||
|
chat_service=services["chat_service"],
|
||||||
|
session_manager=services["session_manager"],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
methods=["GET"],
|
||||||
|
),
|
||||||
|
Route(
|
||||||
|
"/nudges/{chat_id}",
|
||||||
|
require_auth(services["session_manager"])(
|
||||||
|
partial(
|
||||||
|
nudges.nudges_from_chat_id_endpoint,
|
||||||
|
chat_service=services["chat_service"],
|
||||||
|
session_manager=services["session_manager"],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
methods=["GET"],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
app = Starlette(debug=True, routes=routes)
|
app = Starlette(debug=True, routes=routes)
|
||||||
|
|
@ -678,18 +726,30 @@ async def cleanup_subscriptions_proper(services):
|
||||||
|
|
||||||
for connection in active_connections:
|
for connection in active_connections:
|
||||||
try:
|
try:
|
||||||
logger.info("Cancelling subscription for connection", connection_id=connection.connection_id)
|
logger.info(
|
||||||
|
"Cancelling subscription for connection",
|
||||||
|
connection_id=connection.connection_id,
|
||||||
|
)
|
||||||
connector = await connector_service.get_connector(
|
connector = await connector_service.get_connector(
|
||||||
connection.connection_id
|
connection.connection_id
|
||||||
)
|
)
|
||||||
if connector:
|
if connector:
|
||||||
subscription_id = connection.config.get("webhook_channel_id")
|
subscription_id = connection.config.get("webhook_channel_id")
|
||||||
await connector.cleanup_subscription(subscription_id)
|
await connector.cleanup_subscription(subscription_id)
|
||||||
logger.info("Cancelled subscription", subscription_id=subscription_id)
|
logger.info(
|
||||||
|
"Cancelled subscription", subscription_id=subscription_id
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to cancel subscription", connection_id=connection.connection_id, error=str(e))
|
logger.error(
|
||||||
|
"Failed to cancel subscription",
|
||||||
|
connection_id=connection.connection_id,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Finished cancelling subscriptions", subscription_count=len(active_connections))
|
logger.info(
|
||||||
|
"Finished cancelling subscriptions",
|
||||||
|
subscription_count=len(active_connections),
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to cleanup subscriptions", error=str(e))
|
logger.error("Failed to cleanup subscriptions", error=str(e))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
from config.settings import clients, LANGFLOW_URL, FLOW_ID
|
from config.settings import NUDGES_FLOW_ID, clients, LANGFLOW_URL, FLOW_ID
|
||||||
from agent import async_chat, async_langflow, async_chat_stream, async_langflow_stream
|
from agent import (
|
||||||
|
async_chat,
|
||||||
|
async_langflow,
|
||||||
|
async_chat_stream,
|
||||||
|
)
|
||||||
from auth_context import set_auth_context
|
from auth_context import set_auth_context
|
||||||
import json
|
import json
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
@ -111,7 +115,10 @@ class ChatService:
|
||||||
|
|
||||||
# Pass the complete filter expression as a single header to Langflow (only if we have something to send)
|
# Pass the complete filter expression as a single header to Langflow (only if we have something to send)
|
||||||
if filter_expression:
|
if filter_expression:
|
||||||
logger.info("Sending OpenRAG query filter to Langflow", filter_expression=filter_expression)
|
logger.info(
|
||||||
|
"Sending OpenRAG query filter to Langflow",
|
||||||
|
filter_expression=filter_expression,
|
||||||
|
)
|
||||||
extra_headers["X-LANGFLOW-GLOBAL-VAR-OPENRAG-QUERY-FILTER"] = json.dumps(
|
extra_headers["X-LANGFLOW-GLOBAL-VAR-OPENRAG-QUERY-FILTER"] = json.dumps(
|
||||||
filter_expression
|
filter_expression
|
||||||
)
|
)
|
||||||
|
|
@ -150,6 +157,62 @@ class ChatService:
|
||||||
response_data["response_id"] = response_id
|
response_data["response_id"] = response_id
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
async def langflow_nudges_chat(
|
||||||
|
self,
|
||||||
|
user_id: str = None,
|
||||||
|
jwt_token: str = None,
|
||||||
|
previous_response_id: str = None,
|
||||||
|
):
|
||||||
|
"""Handle Langflow chat requests"""
|
||||||
|
|
||||||
|
if not LANGFLOW_URL or not NUDGES_FLOW_ID:
|
||||||
|
raise ValueError(
|
||||||
|
"LANGFLOW_URL and NUDGES_FLOW_ID environment variables are required"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare extra headers for JWT authentication
|
||||||
|
extra_headers = {}
|
||||||
|
if jwt_token:
|
||||||
|
extra_headers["X-LANGFLOW-GLOBAL-VAR-JWT"] = jwt_token
|
||||||
|
|
||||||
|
# Ensure the Langflow client exists; try lazy init if needed
|
||||||
|
langflow_client = await clients.ensure_langflow_client()
|
||||||
|
if not langflow_client:
|
||||||
|
raise ValueError(
|
||||||
|
"Langflow client not initialized. Ensure LANGFLOW is reachable or set LANGFLOW_KEY."
|
||||||
|
)
|
||||||
|
prompt = ""
|
||||||
|
if previous_response_id:
|
||||||
|
from agent import get_conversation_thread
|
||||||
|
|
||||||
|
conversation_history = get_conversation_thread(
|
||||||
|
user_id, previous_response_id
|
||||||
|
)
|
||||||
|
if conversation_history:
|
||||||
|
conversation_history = "\n".join(
|
||||||
|
[
|
||||||
|
f"{msg['role']}: {msg['content']}"
|
||||||
|
for msg in conversation_history["messages"]
|
||||||
|
if msg["role"] in ["user", "assistant"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
prompt = f"{conversation_history}"
|
||||||
|
|
||||||
|
from agent import async_langflow_chat
|
||||||
|
|
||||||
|
response_text, response_id = await async_langflow_chat(
|
||||||
|
langflow_client,
|
||||||
|
NUDGES_FLOW_ID,
|
||||||
|
prompt,
|
||||||
|
user_id,
|
||||||
|
extra_headers=extra_headers,
|
||||||
|
store_conversation=False,
|
||||||
|
)
|
||||||
|
response_data = {"response": response_text}
|
||||||
|
if response_id:
|
||||||
|
response_data["response_id"] = response_id
|
||||||
|
return response_data
|
||||||
|
|
||||||
async def upload_context_chat(
|
async def upload_context_chat(
|
||||||
self,
|
self,
|
||||||
document_content: str,
|
document_content: str,
|
||||||
|
|
@ -201,7 +264,11 @@ class ChatService:
|
||||||
return {"error": "User ID is required", "conversations": []}
|
return {"error": "User ID is required", "conversations": []}
|
||||||
|
|
||||||
conversations_dict = get_user_conversations(user_id)
|
conversations_dict = get_user_conversations(user_id)
|
||||||
logger.debug("Getting chat history for user", user_id=user_id, conversation_count=len(conversations_dict))
|
logger.debug(
|
||||||
|
"Getting chat history for user",
|
||||||
|
user_id=user_id,
|
||||||
|
conversation_count=len(conversations_dict),
|
||||||
|
)
|
||||||
|
|
||||||
# Convert conversations dict to list format with metadata
|
# Convert conversations dict to list format with metadata
|
||||||
conversations = []
|
conversations = []
|
||||||
|
|
@ -270,16 +337,16 @@ class ChatService:
|
||||||
from agent import get_user_conversations
|
from agent import get_user_conversations
|
||||||
from services.langflow_history_service import langflow_history_service
|
from services.langflow_history_service import langflow_history_service
|
||||||
from services.user_binding_service import user_binding_service
|
from services.user_binding_service import user_binding_service
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {"error": "User ID is required", "conversations": []}
|
return {"error": "User ID is required", "conversations": []}
|
||||||
|
|
||||||
all_conversations = []
|
all_conversations = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Get in-memory OpenRAG conversations (current session)
|
# 1. Get in-memory OpenRAG conversations (current session)
|
||||||
conversations_dict = get_user_conversations(user_id)
|
conversations_dict = get_user_conversations(user_id)
|
||||||
|
|
||||||
for response_id, conversation_state in conversations_dict.items():
|
for response_id, conversation_state in conversations_dict.items():
|
||||||
# Filter out system messages
|
# Filter out system messages
|
||||||
messages = []
|
messages = []
|
||||||
|
|
@ -295,7 +362,7 @@ class ChatService:
|
||||||
if msg.get("response_id"):
|
if msg.get("response_id"):
|
||||||
message_data["response_id"] = msg["response_id"]
|
message_data["response_id"] = msg["response_id"]
|
||||||
messages.append(message_data)
|
messages.append(message_data)
|
||||||
|
|
||||||
if messages: # Only include conversations with actual messages
|
if messages: # Only include conversations with actual messages
|
||||||
# Generate title from first user message
|
# Generate title from first user message
|
||||||
first_user_msg = next(
|
first_user_msg = next(
|
||||||
|
|
@ -308,43 +375,59 @@ class ChatService:
|
||||||
if first_user_msg
|
if first_user_msg
|
||||||
else "New chat"
|
else "New chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
all_conversations.append({
|
all_conversations.append(
|
||||||
"response_id": response_id,
|
{
|
||||||
"title": title,
|
"response_id": response_id,
|
||||||
"endpoint": "langflow",
|
"title": title,
|
||||||
"messages": messages,
|
"endpoint": "langflow",
|
||||||
"created_at": conversation_state.get("created_at").isoformat()
|
"messages": messages,
|
||||||
if conversation_state.get("created_at")
|
"created_at": conversation_state.get(
|
||||||
else None,
|
"created_at"
|
||||||
"last_activity": conversation_state.get("last_activity").isoformat()
|
).isoformat()
|
||||||
if conversation_state.get("last_activity")
|
if conversation_state.get("created_at")
|
||||||
else None,
|
else None,
|
||||||
"previous_response_id": conversation_state.get("previous_response_id"),
|
"last_activity": conversation_state.get(
|
||||||
"total_messages": len(messages),
|
"last_activity"
|
||||||
"source": "openrag_memory"
|
).isoformat()
|
||||||
})
|
if conversation_state.get("last_activity")
|
||||||
|
else None,
|
||||||
# 2. Get historical conversations from Langflow database
|
"previous_response_id": conversation_state.get(
|
||||||
|
"previous_response_id"
|
||||||
|
),
|
||||||
|
"total_messages": len(messages),
|
||||||
|
"source": "openrag_memory",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Get historical conversations from Langflow database
|
||||||
# (works with both Google-bound users and direct Langflow users)
|
# (works with both Google-bound users and direct Langflow users)
|
||||||
print(f"[DEBUG] Attempting to fetch Langflow history for user: {user_id}")
|
print(f"[DEBUG] Attempting to fetch Langflow history for user: {user_id}")
|
||||||
langflow_history = await langflow_history_service.get_user_conversation_history(user_id, flow_id=FLOW_ID)
|
langflow_history = (
|
||||||
|
await langflow_history_service.get_user_conversation_history(
|
||||||
|
user_id, flow_id=FLOW_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if langflow_history.get("conversations"):
|
if langflow_history.get("conversations"):
|
||||||
for conversation in langflow_history["conversations"]:
|
for conversation in langflow_history["conversations"]:
|
||||||
# Convert Langflow format to OpenRAG format
|
# Convert Langflow format to OpenRAG format
|
||||||
messages = []
|
messages = []
|
||||||
for msg in conversation.get("messages", []):
|
for msg in conversation.get("messages", []):
|
||||||
messages.append({
|
messages.append(
|
||||||
"role": msg["role"],
|
{
|
||||||
"content": msg["content"],
|
"role": msg["role"],
|
||||||
"timestamp": msg.get("timestamp"),
|
"content": msg["content"],
|
||||||
"langflow_message_id": msg.get("langflow_message_id"),
|
"timestamp": msg.get("timestamp"),
|
||||||
"source": "langflow"
|
"langflow_message_id": msg.get("langflow_message_id"),
|
||||||
})
|
"source": "langflow",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if messages:
|
if messages:
|
||||||
first_user_msg = next((msg for msg in messages if msg["role"] == "user"), None)
|
first_user_msg = next(
|
||||||
|
(msg for msg in messages if msg["role"] == "user"), None
|
||||||
|
)
|
||||||
title = (
|
title = (
|
||||||
first_user_msg["content"][:50] + "..."
|
first_user_msg["content"][:50] + "..."
|
||||||
if first_user_msg and len(first_user_msg["content"]) > 50
|
if first_user_msg and len(first_user_msg["content"]) > 50
|
||||||
|
|
@ -352,33 +435,39 @@ class ChatService:
|
||||||
if first_user_msg
|
if first_user_msg
|
||||||
else "Langflow chat"
|
else "Langflow chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
all_conversations.append({
|
all_conversations.append(
|
||||||
"response_id": conversation["session_id"],
|
{
|
||||||
"title": title,
|
"response_id": conversation["session_id"],
|
||||||
"endpoint": "langflow",
|
"title": title,
|
||||||
"messages": messages,
|
"endpoint": "langflow",
|
||||||
"created_at": conversation.get("created_at"),
|
"messages": messages,
|
||||||
"last_activity": conversation.get("last_activity"),
|
"created_at": conversation.get("created_at"),
|
||||||
"total_messages": len(messages),
|
"last_activity": conversation.get("last_activity"),
|
||||||
"source": "langflow_database",
|
"total_messages": len(messages),
|
||||||
"langflow_session_id": conversation["session_id"],
|
"source": "langflow_database",
|
||||||
"langflow_flow_id": conversation.get("flow_id")
|
"langflow_session_id": conversation["session_id"],
|
||||||
})
|
"langflow_flow_id": conversation.get("flow_id"),
|
||||||
|
}
|
||||||
print(f"[DEBUG] Added {len(langflow_history['conversations'])} historical conversations from Langflow")
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[DEBUG] Added {len(langflow_history['conversations'])} historical conversations from Langflow"
|
||||||
|
)
|
||||||
elif langflow_history.get("error"):
|
elif langflow_history.get("error"):
|
||||||
print(f"[DEBUG] Could not fetch Langflow history for user {user_id}: {langflow_history['error']}")
|
print(
|
||||||
|
f"[DEBUG] Could not fetch Langflow history for user {user_id}: {langflow_history['error']}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"[DEBUG] No Langflow conversations found for user {user_id}")
|
print(f"[DEBUG] No Langflow conversations found for user {user_id}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to fetch Langflow history: {e}")
|
print(f"[ERROR] Failed to fetch Langflow history: {e}")
|
||||||
# Continue with just in-memory conversations
|
# Continue with just in-memory conversations
|
||||||
|
|
||||||
# Deduplicate conversations by response_id (in-memory takes priority over database)
|
# Deduplicate conversations by response_id (in-memory takes priority over database)
|
||||||
deduplicated_conversations = {}
|
deduplicated_conversations = {}
|
||||||
|
|
||||||
for conversation in all_conversations:
|
for conversation in all_conversations:
|
||||||
response_id = conversation.get("response_id")
|
response_id = conversation.get("response_id")
|
||||||
if response_id:
|
if response_id:
|
||||||
|
|
@ -390,37 +479,52 @@ class ChatService:
|
||||||
existing = deduplicated_conversations[response_id]
|
existing = deduplicated_conversations[response_id]
|
||||||
current_source = conversation.get("source")
|
current_source = conversation.get("source")
|
||||||
existing_source = existing.get("source")
|
existing_source = existing.get("source")
|
||||||
|
|
||||||
if current_source == "openrag_memory" and existing_source == "langflow_database":
|
if (
|
||||||
|
current_source == "openrag_memory"
|
||||||
|
and existing_source == "langflow_database"
|
||||||
|
):
|
||||||
# Replace database version with in-memory version
|
# Replace database version with in-memory version
|
||||||
deduplicated_conversations[response_id] = conversation
|
deduplicated_conversations[response_id] = conversation
|
||||||
print(f"[DEBUG] Replaced database conversation {response_id} with in-memory version")
|
print(
|
||||||
|
f"[DEBUG] Replaced database conversation {response_id} with in-memory version"
|
||||||
|
)
|
||||||
# Otherwise keep existing (in-memory has priority, or first database entry)
|
# Otherwise keep existing (in-memory has priority, or first database entry)
|
||||||
else:
|
else:
|
||||||
# No response_id - add with unique key based on content and timestamp
|
# No response_id - add with unique key based on content and timestamp
|
||||||
unique_key = f"no_id_{hash(conversation.get('title', ''))}{conversation.get('created_at', '')}"
|
unique_key = f"no_id_{hash(conversation.get('title', ''))}{conversation.get('created_at', '')}"
|
||||||
if unique_key not in deduplicated_conversations:
|
if unique_key not in deduplicated_conversations:
|
||||||
deduplicated_conversations[unique_key] = conversation
|
deduplicated_conversations[unique_key] = conversation
|
||||||
|
|
||||||
final_conversations = list(deduplicated_conversations.values())
|
final_conversations = list(deduplicated_conversations.values())
|
||||||
|
|
||||||
# Sort by last activity (most recent first)
|
# Sort by last activity (most recent first)
|
||||||
final_conversations.sort(key=lambda c: c.get("last_activity", ""), reverse=True)
|
final_conversations.sort(key=lambda c: c.get("last_activity", ""), reverse=True)
|
||||||
|
|
||||||
# Calculate source statistics after deduplication
|
# Calculate source statistics after deduplication
|
||||||
sources = {
|
sources = {
|
||||||
"memory": len([c for c in final_conversations if c.get("source") == "openrag_memory"]),
|
"memory": len(
|
||||||
"langflow_db": len([c for c in final_conversations if c.get("source") == "langflow_database"]),
|
[c for c in final_conversations if c.get("source") == "openrag_memory"]
|
||||||
"duplicates_removed": len(all_conversations) - len(final_conversations)
|
),
|
||||||
|
"langflow_db": len(
|
||||||
|
[
|
||||||
|
c
|
||||||
|
for c in final_conversations
|
||||||
|
if c.get("source") == "langflow_database"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"duplicates_removed": len(all_conversations) - len(final_conversations),
|
||||||
}
|
}
|
||||||
|
|
||||||
if sources["duplicates_removed"] > 0:
|
if sources["duplicates_removed"] > 0:
|
||||||
print(f"[DEBUG] Removed {sources['duplicates_removed']} duplicate conversations")
|
print(
|
||||||
|
f"[DEBUG] Removed {sources['duplicates_removed']} duplicate conversations"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"endpoint": "langflow",
|
"endpoint": "langflow",
|
||||||
"conversations": final_conversations,
|
"conversations": final_conversations,
|
||||||
"total_conversations": len(final_conversations),
|
"total_conversations": len(final_conversations),
|
||||||
"sources": sources
|
"sources": sources,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,14 @@ from ..utils.validation import (
|
||||||
validate_non_empty,
|
validate_non_empty,
|
||||||
validate_url,
|
validate_url,
|
||||||
validate_documents_paths,
|
validate_documents_paths,
|
||||||
sanitize_env_value
|
sanitize_env_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnvConfig:
|
class EnvConfig:
|
||||||
"""Environment configuration data."""
|
"""Environment configuration data."""
|
||||||
|
|
||||||
# Core settings
|
# Core settings
|
||||||
openai_api_key: str = ""
|
openai_api_key: str = ""
|
||||||
opensearch_password: str = ""
|
opensearch_password: str = ""
|
||||||
|
|
@ -27,155 +28,187 @@ class EnvConfig:
|
||||||
langflow_superuser: str = "admin"
|
langflow_superuser: str = "admin"
|
||||||
langflow_superuser_password: str = ""
|
langflow_superuser_password: str = ""
|
||||||
flow_id: str = "1098eea1-6649-4e1d-aed1-b77249fb8dd0"
|
flow_id: str = "1098eea1-6649-4e1d-aed1-b77249fb8dd0"
|
||||||
|
|
||||||
# OAuth settings
|
# OAuth settings
|
||||||
google_oauth_client_id: str = ""
|
google_oauth_client_id: str = ""
|
||||||
google_oauth_client_secret: str = ""
|
google_oauth_client_secret: str = ""
|
||||||
microsoft_graph_oauth_client_id: str = ""
|
microsoft_graph_oauth_client_id: str = ""
|
||||||
microsoft_graph_oauth_client_secret: str = ""
|
microsoft_graph_oauth_client_secret: str = ""
|
||||||
|
|
||||||
# Optional settings
|
# Optional settings
|
||||||
webhook_base_url: str = ""
|
webhook_base_url: str = ""
|
||||||
aws_access_key_id: str = ""
|
aws_access_key_id: str = ""
|
||||||
aws_secret_access_key: str = ""
|
aws_secret_access_key: str = ""
|
||||||
langflow_public_url: str = ""
|
langflow_public_url: str = ""
|
||||||
|
|
||||||
# Langflow auth settings
|
# Langflow auth settings
|
||||||
langflow_auto_login: str = "False"
|
langflow_auto_login: str = "False"
|
||||||
langflow_new_user_is_active: str = "False"
|
langflow_new_user_is_active: str = "False"
|
||||||
langflow_enable_superuser_cli: str = "False"
|
langflow_enable_superuser_cli: str = "False"
|
||||||
|
|
||||||
# Document paths (comma-separated)
|
# Document paths (comma-separated)
|
||||||
openrag_documents_paths: str = "./documents"
|
openrag_documents_paths: str = "./documents"
|
||||||
|
|
||||||
# Validation errors
|
# Validation errors
|
||||||
validation_errors: Dict[str, str] = field(default_factory=dict)
|
validation_errors: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class EnvManager:
|
class EnvManager:
|
||||||
"""Manages environment configuration for OpenRAG."""
|
"""Manages environment configuration for OpenRAG."""
|
||||||
|
|
||||||
def __init__(self, env_file: Optional[Path] = None):
|
def __init__(self, env_file: Optional[Path] = None):
|
||||||
self.env_file = env_file or Path(".env")
|
self.env_file = env_file or Path(".env")
|
||||||
self.config = EnvConfig()
|
self.config = EnvConfig()
|
||||||
|
|
||||||
def generate_secure_password(self) -> str:
|
def generate_secure_password(self) -> str:
|
||||||
"""Generate a secure password for OpenSearch."""
|
"""Generate a secure password for OpenSearch."""
|
||||||
# Generate a 16-character password with letters, digits, and symbols
|
# Generate a 16-character password with letters, digits, and symbols
|
||||||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||||
return ''.join(secrets.choice(alphabet) for _ in range(16))
|
return "".join(secrets.choice(alphabet) for _ in range(16))
|
||||||
|
|
||||||
def generate_langflow_secret_key(self) -> str:
|
def generate_langflow_secret_key(self) -> str:
|
||||||
"""Generate a secure secret key for Langflow."""
|
"""Generate a secure secret key for Langflow."""
|
||||||
return secrets.token_urlsafe(32)
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
def load_existing_env(self) -> bool:
|
def load_existing_env(self) -> bool:
|
||||||
"""Load existing .env file if it exists."""
|
"""Load existing .env file if it exists."""
|
||||||
if not self.env_file.exists():
|
if not self.env_file.exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.env_file, 'r') as f:
|
with open(self.env_file, "r") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith('#'):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if '=' in line:
|
if "=" in line:
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split("=", 1)
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
value = sanitize_env_value(value)
|
value = sanitize_env_value(value)
|
||||||
|
|
||||||
# Map env vars to config attributes
|
# Map env vars to config attributes
|
||||||
attr_map = {
|
attr_map = {
|
||||||
'OPENAI_API_KEY': 'openai_api_key',
|
"OPENAI_API_KEY": "openai_api_key",
|
||||||
'OPENSEARCH_PASSWORD': 'opensearch_password',
|
"OPENSEARCH_PASSWORD": "opensearch_password",
|
||||||
'LANGFLOW_SECRET_KEY': 'langflow_secret_key',
|
"LANGFLOW_SECRET_KEY": "langflow_secret_key",
|
||||||
'LANGFLOW_SUPERUSER': 'langflow_superuser',
|
"LANGFLOW_SUPERUSER": "langflow_superuser",
|
||||||
'LANGFLOW_SUPERUSER_PASSWORD': 'langflow_superuser_password',
|
"LANGFLOW_SUPERUSER_PASSWORD": "langflow_superuser_password",
|
||||||
'FLOW_ID': 'flow_id',
|
"FLOW_ID": "flow_id",
|
||||||
'GOOGLE_OAUTH_CLIENT_ID': 'google_oauth_client_id',
|
"NUDGES_FLOW_ID": "nudges_flow_id",
|
||||||
'GOOGLE_OAUTH_CLIENT_SECRET': 'google_oauth_client_secret',
|
"GOOGLE_OAUTH_CLIENT_ID": "google_oauth_client_id",
|
||||||
'MICROSOFT_GRAPH_OAUTH_CLIENT_ID': 'microsoft_graph_oauth_client_id',
|
"GOOGLE_OAUTH_CLIENT_SECRET": "google_oauth_client_secret",
|
||||||
'MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET': 'microsoft_graph_oauth_client_secret',
|
"MICROSOFT_GRAPH_OAUTH_CLIENT_ID": "microsoft_graph_oauth_client_id",
|
||||||
'WEBHOOK_BASE_URL': 'webhook_base_url',
|
"MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET": "microsoft_graph_oauth_client_secret",
|
||||||
'AWS_ACCESS_KEY_ID': 'aws_access_key_id',
|
"WEBHOOK_BASE_URL": "webhook_base_url",
|
||||||
'AWS_SECRET_ACCESS_KEY': 'aws_secret_access_key',
|
"AWS_ACCESS_KEY_ID": "aws_access_key_id",
|
||||||
'LANGFLOW_PUBLIC_URL': 'langflow_public_url',
|
"AWS_SECRET_ACCESS_KEY": "aws_secret_access_key",
|
||||||
'OPENRAG_DOCUMENTS_PATHS': 'openrag_documents_paths',
|
"LANGFLOW_PUBLIC_URL": "langflow_public_url",
|
||||||
'LANGFLOW_AUTO_LOGIN': 'langflow_auto_login',
|
"OPENRAG_DOCUMENTS_PATHS": "openrag_documents_paths",
|
||||||
'LANGFLOW_NEW_USER_IS_ACTIVE': 'langflow_new_user_is_active',
|
"LANGFLOW_AUTO_LOGIN": "langflow_auto_login",
|
||||||
'LANGFLOW_ENABLE_SUPERUSER_CLI': 'langflow_enable_superuser_cli',
|
"LANGFLOW_NEW_USER_IS_ACTIVE": "langflow_new_user_is_active",
|
||||||
|
"LANGFLOW_ENABLE_SUPERUSER_CLI": "langflow_enable_superuser_cli",
|
||||||
}
|
}
|
||||||
|
|
||||||
if key in attr_map:
|
if key in attr_map:
|
||||||
setattr(self.config, attr_map[key], value)
|
setattr(self.config, attr_map[key], value)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading .env file: {e}")
|
print(f"Error loading .env file: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def setup_secure_defaults(self) -> None:
|
def setup_secure_defaults(self) -> None:
|
||||||
"""Set up secure default values for passwords and keys."""
|
"""Set up secure default values for passwords and keys."""
|
||||||
if not self.config.opensearch_password:
|
if not self.config.opensearch_password:
|
||||||
self.config.opensearch_password = self.generate_secure_password()
|
self.config.opensearch_password = self.generate_secure_password()
|
||||||
|
|
||||||
if not self.config.langflow_secret_key:
|
if not self.config.langflow_secret_key:
|
||||||
self.config.langflow_secret_key = self.generate_langflow_secret_key()
|
self.config.langflow_secret_key = self.generate_langflow_secret_key()
|
||||||
|
|
||||||
if not self.config.langflow_superuser_password:
|
if not self.config.langflow_superuser_password:
|
||||||
self.config.langflow_superuser_password = self.generate_secure_password()
|
self.config.langflow_superuser_password = self.generate_secure_password()
|
||||||
|
|
||||||
def validate_config(self, mode: str = "full") -> bool:
|
def validate_config(self, mode: str = "full") -> bool:
|
||||||
"""
|
"""
|
||||||
Validate the current configuration.
|
Validate the current configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode: "no_auth" for minimal validation, "full" for complete validation
|
mode: "no_auth" for minimal validation, "full" for complete validation
|
||||||
"""
|
"""
|
||||||
self.config.validation_errors.clear()
|
self.config.validation_errors.clear()
|
||||||
|
|
||||||
# Always validate OpenAI API key
|
# Always validate OpenAI API key
|
||||||
if not validate_openai_api_key(self.config.openai_api_key):
|
if not validate_openai_api_key(self.config.openai_api_key):
|
||||||
self.config.validation_errors['openai_api_key'] = "Invalid OpenAI API key format (should start with sk-)"
|
self.config.validation_errors["openai_api_key"] = (
|
||||||
|
"Invalid OpenAI API key format (should start with sk-)"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate documents paths only if provided (optional)
|
# Validate documents paths only if provided (optional)
|
||||||
if self.config.openrag_documents_paths:
|
if self.config.openrag_documents_paths:
|
||||||
is_valid, error_msg, _ = validate_documents_paths(self.config.openrag_documents_paths)
|
is_valid, error_msg, _ = validate_documents_paths(
|
||||||
|
self.config.openrag_documents_paths
|
||||||
|
)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
self.config.validation_errors['openrag_documents_paths'] = error_msg
|
self.config.validation_errors["openrag_documents_paths"] = error_msg
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if not validate_non_empty(self.config.opensearch_password):
|
if not validate_non_empty(self.config.opensearch_password):
|
||||||
self.config.validation_errors['opensearch_password'] = "OpenSearch password is required"
|
self.config.validation_errors["opensearch_password"] = (
|
||||||
|
"OpenSearch password is required"
|
||||||
|
)
|
||||||
|
|
||||||
# Langflow secret key is auto-generated; no user input required
|
# Langflow secret key is auto-generated; no user input required
|
||||||
|
|
||||||
if not validate_non_empty(self.config.langflow_superuser_password):
|
if not validate_non_empty(self.config.langflow_superuser_password):
|
||||||
self.config.validation_errors['langflow_superuser_password'] = "Langflow superuser password is required"
|
self.config.validation_errors["langflow_superuser_password"] = (
|
||||||
|
"Langflow superuser password is required"
|
||||||
|
)
|
||||||
|
|
||||||
if mode == "full":
|
if mode == "full":
|
||||||
# Validate OAuth settings if provided
|
# Validate OAuth settings if provided
|
||||||
if self.config.google_oauth_client_id and not validate_google_oauth_client_id(self.config.google_oauth_client_id):
|
if (
|
||||||
self.config.validation_errors['google_oauth_client_id'] = "Invalid Google OAuth client ID format"
|
self.config.google_oauth_client_id
|
||||||
|
and not validate_google_oauth_client_id(
|
||||||
if self.config.google_oauth_client_id and not validate_non_empty(self.config.google_oauth_client_secret):
|
self.config.google_oauth_client_id
|
||||||
self.config.validation_errors['google_oauth_client_secret'] = "Google OAuth client secret required when client ID is provided"
|
)
|
||||||
|
):
|
||||||
if self.config.microsoft_graph_oauth_client_id and not validate_non_empty(self.config.microsoft_graph_oauth_client_secret):
|
self.config.validation_errors["google_oauth_client_id"] = (
|
||||||
self.config.validation_errors['microsoft_graph_oauth_client_secret'] = "Microsoft Graph client secret required when client ID is provided"
|
"Invalid Google OAuth client ID format"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.config.google_oauth_client_id and not validate_non_empty(
|
||||||
|
self.config.google_oauth_client_secret
|
||||||
|
):
|
||||||
|
self.config.validation_errors["google_oauth_client_secret"] = (
|
||||||
|
"Google OAuth client secret required when client ID is provided"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.config.microsoft_graph_oauth_client_id and not validate_non_empty(
|
||||||
|
self.config.microsoft_graph_oauth_client_secret
|
||||||
|
):
|
||||||
|
self.config.validation_errors["microsoft_graph_oauth_client_secret"] = (
|
||||||
|
"Microsoft Graph client secret required when client ID is provided"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate optional URLs if provided
|
# Validate optional URLs if provided
|
||||||
if self.config.webhook_base_url and not validate_url(self.config.webhook_base_url):
|
if self.config.webhook_base_url and not validate_url(
|
||||||
self.config.validation_errors['webhook_base_url'] = "Invalid webhook URL format"
|
self.config.webhook_base_url
|
||||||
|
):
|
||||||
if self.config.langflow_public_url and not validate_url(self.config.langflow_public_url):
|
self.config.validation_errors["webhook_base_url"] = (
|
||||||
self.config.validation_errors['langflow_public_url'] = "Invalid Langflow public URL format"
|
"Invalid webhook URL format"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.config.langflow_public_url and not validate_url(
|
||||||
|
self.config.langflow_public_url
|
||||||
|
):
|
||||||
|
self.config.validation_errors["langflow_public_url"] = (
|
||||||
|
"Invalid Langflow public URL format"
|
||||||
|
)
|
||||||
|
|
||||||
return len(self.config.validation_errors) == 0
|
return len(self.config.validation_errors) == 0
|
||||||
|
|
||||||
def save_env_file(self) -> bool:
|
def save_env_file(self) -> bool:
|
||||||
"""Save current configuration to .env file."""
|
"""Save current configuration to .env file."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -183,44 +216,67 @@ class EnvManager:
|
||||||
self.setup_secure_defaults()
|
self.setup_secure_defaults()
|
||||||
# Create backup if file exists
|
# Create backup if file exists
|
||||||
if self.env_file.exists():
|
if self.env_file.exists():
|
||||||
backup_file = self.env_file.with_suffix('.env.backup')
|
backup_file = self.env_file.with_suffix(".env.backup")
|
||||||
self.env_file.rename(backup_file)
|
self.env_file.rename(backup_file)
|
||||||
|
|
||||||
with open(self.env_file, 'w') as f:
|
with open(self.env_file, "w") as f:
|
||||||
f.write("# OpenRAG Environment Configuration\n")
|
f.write("# OpenRAG Environment Configuration\n")
|
||||||
f.write("# Generated by OpenRAG TUI\n\n")
|
f.write("# Generated by OpenRAG TUI\n\n")
|
||||||
|
|
||||||
# Core settings
|
# Core settings
|
||||||
f.write("# Core settings\n")
|
f.write("# Core settings\n")
|
||||||
f.write(f"LANGFLOW_SECRET_KEY={self.config.langflow_secret_key}\n")
|
f.write(f"LANGFLOW_SECRET_KEY={self.config.langflow_secret_key}\n")
|
||||||
f.write(f"LANGFLOW_SUPERUSER={self.config.langflow_superuser}\n")
|
f.write(f"LANGFLOW_SUPERUSER={self.config.langflow_superuser}\n")
|
||||||
f.write(f"LANGFLOW_SUPERUSER_PASSWORD={self.config.langflow_superuser_password}\n")
|
f.write(
|
||||||
|
f"LANGFLOW_SUPERUSER_PASSWORD={self.config.langflow_superuser_password}\n"
|
||||||
|
)
|
||||||
f.write(f"FLOW_ID={self.config.flow_id}\n")
|
f.write(f"FLOW_ID={self.config.flow_id}\n")
|
||||||
|
f.write(f"NUDGES_FLOW_ID={self.config.nudges_flow_id}\n")
|
||||||
f.write(f"OPENSEARCH_PASSWORD={self.config.opensearch_password}\n")
|
f.write(f"OPENSEARCH_PASSWORD={self.config.opensearch_password}\n")
|
||||||
f.write(f"OPENAI_API_KEY={self.config.openai_api_key}\n")
|
f.write(f"OPENAI_API_KEY={self.config.openai_api_key}\n")
|
||||||
f.write(f"OPENRAG_DOCUMENTS_PATHS={self.config.openrag_documents_paths}\n")
|
f.write(
|
||||||
|
f"OPENRAG_DOCUMENTS_PATHS={self.config.openrag_documents_paths}\n"
|
||||||
|
)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
# Langflow auth settings
|
# Langflow auth settings
|
||||||
f.write("# Langflow auth settings\n")
|
f.write("# Langflow auth settings\n")
|
||||||
f.write(f"LANGFLOW_AUTO_LOGIN={self.config.langflow_auto_login}\n")
|
f.write(f"LANGFLOW_AUTO_LOGIN={self.config.langflow_auto_login}\n")
|
||||||
f.write(f"LANGFLOW_NEW_USER_IS_ACTIVE={self.config.langflow_new_user_is_active}\n")
|
f.write(
|
||||||
f.write(f"LANGFLOW_ENABLE_SUPERUSER_CLI={self.config.langflow_enable_superuser_cli}\n")
|
f"LANGFLOW_NEW_USER_IS_ACTIVE={self.config.langflow_new_user_is_active}\n"
|
||||||
|
)
|
||||||
|
f.write(
|
||||||
|
f"LANGFLOW_ENABLE_SUPERUSER_CLI={self.config.langflow_enable_superuser_cli}\n"
|
||||||
|
)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
# OAuth settings
|
# OAuth settings
|
||||||
if self.config.google_oauth_client_id or self.config.google_oauth_client_secret:
|
if (
|
||||||
|
self.config.google_oauth_client_id
|
||||||
|
or self.config.google_oauth_client_secret
|
||||||
|
):
|
||||||
f.write("# Google OAuth settings\n")
|
f.write("# Google OAuth settings\n")
|
||||||
f.write(f"GOOGLE_OAUTH_CLIENT_ID={self.config.google_oauth_client_id}\n")
|
f.write(
|
||||||
f.write(f"GOOGLE_OAUTH_CLIENT_SECRET={self.config.google_oauth_client_secret}\n")
|
f"GOOGLE_OAUTH_CLIENT_ID={self.config.google_oauth_client_id}\n"
|
||||||
|
)
|
||||||
|
f.write(
|
||||||
|
f"GOOGLE_OAUTH_CLIENT_SECRET={self.config.google_oauth_client_secret}\n"
|
||||||
|
)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
if self.config.microsoft_graph_oauth_client_id or self.config.microsoft_graph_oauth_client_secret:
|
if (
|
||||||
|
self.config.microsoft_graph_oauth_client_id
|
||||||
|
or self.config.microsoft_graph_oauth_client_secret
|
||||||
|
):
|
||||||
f.write("# Microsoft Graph OAuth settings\n")
|
f.write("# Microsoft Graph OAuth settings\n")
|
||||||
f.write(f"MICROSOFT_GRAPH_OAUTH_CLIENT_ID={self.config.microsoft_graph_oauth_client_id}\n")
|
f.write(
|
||||||
f.write(f"MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET={self.config.microsoft_graph_oauth_client_secret}\n")
|
f"MICROSOFT_GRAPH_OAUTH_CLIENT_ID={self.config.microsoft_graph_oauth_client_id}\n"
|
||||||
|
)
|
||||||
|
f.write(
|
||||||
|
f"MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET={self.config.microsoft_graph_oauth_client_secret}\n"
|
||||||
|
)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
# Optional settings
|
# Optional settings
|
||||||
optional_vars = [
|
optional_vars = [
|
||||||
("WEBHOOK_BASE_URL", self.config.webhook_base_url),
|
("WEBHOOK_BASE_URL", self.config.webhook_base_url),
|
||||||
|
|
@ -228,7 +284,7 @@ class EnvManager:
|
||||||
("AWS_SECRET_ACCESS_KEY", self.config.aws_secret_access_key),
|
("AWS_SECRET_ACCESS_KEY", self.config.aws_secret_access_key),
|
||||||
("LANGFLOW_PUBLIC_URL", self.config.langflow_public_url),
|
("LANGFLOW_PUBLIC_URL", self.config.langflow_public_url),
|
||||||
]
|
]
|
||||||
|
|
||||||
optional_written = False
|
optional_written = False
|
||||||
for var_name, var_value in optional_vars:
|
for var_name, var_value in optional_vars:
|
||||||
if var_value:
|
if var_value:
|
||||||
|
|
@ -236,52 +292,89 @@ class EnvManager:
|
||||||
f.write("# Optional settings\n")
|
f.write("# Optional settings\n")
|
||||||
optional_written = True
|
optional_written = True
|
||||||
f.write(f"{var_name}={var_value}\n")
|
f.write(f"{var_name}={var_value}\n")
|
||||||
|
|
||||||
if optional_written:
|
if optional_written:
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving .env file: {e}")
|
print(f"Error saving .env file: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_no_auth_setup_fields(self) -> List[tuple[str, str, str, bool]]:
|
def get_no_auth_setup_fields(self) -> List[tuple[str, str, str, bool]]:
|
||||||
"""Get fields required for no-auth setup mode. Returns (field_name, display_name, placeholder, can_generate)."""
|
"""Get fields required for no-auth setup mode. Returns (field_name, display_name, placeholder, can_generate)."""
|
||||||
return [
|
return [
|
||||||
("openai_api_key", "OpenAI API Key", "sk-...", False),
|
("openai_api_key", "OpenAI API Key", "sk-...", False),
|
||||||
("opensearch_password", "OpenSearch Password", "Will be auto-generated if empty", True),
|
(
|
||||||
("langflow_superuser_password", "Langflow Superuser Password", "Will be auto-generated if empty", True),
|
"opensearch_password",
|
||||||
("openrag_documents_paths", "Documents Paths", "./documents,/path/to/more/docs", False),
|
"OpenSearch Password",
|
||||||
|
"Will be auto-generated if empty",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"langflow_superuser_password",
|
||||||
|
"Langflow Superuser Password",
|
||||||
|
"Will be auto-generated if empty",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"openrag_documents_paths",
|
||||||
|
"Documents Paths",
|
||||||
|
"./documents,/path/to/more/docs",
|
||||||
|
False,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_full_setup_fields(self) -> List[tuple[str, str, str, bool]]:
|
def get_full_setup_fields(self) -> List[tuple[str, str, str, bool]]:
|
||||||
"""Get all fields for full setup mode."""
|
"""Get all fields for full setup mode."""
|
||||||
base_fields = self.get_no_auth_setup_fields()
|
base_fields = self.get_no_auth_setup_fields()
|
||||||
|
|
||||||
oauth_fields = [
|
oauth_fields = [
|
||||||
("google_oauth_client_id", "Google OAuth Client ID", "xxx.apps.googleusercontent.com", False),
|
(
|
||||||
|
"google_oauth_client_id",
|
||||||
|
"Google OAuth Client ID",
|
||||||
|
"xxx.apps.googleusercontent.com",
|
||||||
|
False,
|
||||||
|
),
|
||||||
("google_oauth_client_secret", "Google OAuth Client Secret", "", False),
|
("google_oauth_client_secret", "Google OAuth Client Secret", "", False),
|
||||||
("microsoft_graph_oauth_client_id", "Microsoft Graph Client ID", "", False),
|
("microsoft_graph_oauth_client_id", "Microsoft Graph Client ID", "", False),
|
||||||
("microsoft_graph_oauth_client_secret", "Microsoft Graph Client Secret", "", False),
|
(
|
||||||
|
"microsoft_graph_oauth_client_secret",
|
||||||
|
"Microsoft Graph Client Secret",
|
||||||
|
"",
|
||||||
|
False,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
optional_fields = [
|
optional_fields = [
|
||||||
("webhook_base_url", "Webhook Base URL (optional)", "https://your-domain.com", False),
|
(
|
||||||
|
"webhook_base_url",
|
||||||
|
"Webhook Base URL (optional)",
|
||||||
|
"https://your-domain.com",
|
||||||
|
False,
|
||||||
|
),
|
||||||
("aws_access_key_id", "AWS Access Key ID (optional)", "", False),
|
("aws_access_key_id", "AWS Access Key ID (optional)", "", False),
|
||||||
("aws_secret_access_key", "AWS Secret Access Key (optional)", "", False),
|
("aws_secret_access_key", "AWS Secret Access Key (optional)", "", False),
|
||||||
("langflow_public_url", "Langflow Public URL (optional)", "http://localhost:7860", False),
|
(
|
||||||
|
"langflow_public_url",
|
||||||
|
"Langflow Public URL (optional)",
|
||||||
|
"http://localhost:7860",
|
||||||
|
False,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
return base_fields + oauth_fields + optional_fields
|
return base_fields + oauth_fields + optional_fields
|
||||||
|
|
||||||
def generate_compose_volume_mounts(self) -> List[str]:
|
def generate_compose_volume_mounts(self) -> List[str]:
|
||||||
"""Generate Docker Compose volume mount strings from documents paths."""
|
"""Generate Docker Compose volume mount strings from documents paths."""
|
||||||
is_valid, _, validated_paths = validate_documents_paths(self.config.openrag_documents_paths)
|
is_valid, _, validated_paths = validate_documents_paths(
|
||||||
|
self.config.openrag_documents_paths
|
||||||
|
)
|
||||||
|
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return ["./documents:/app/documents:Z"] # fallback
|
return ["./documents:/app/documents:Z"] # fallback
|
||||||
|
|
||||||
volume_mounts = []
|
volume_mounts = []
|
||||||
for i, path in enumerate(validated_paths):
|
for i, path in enumerate(validated_paths):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
|
|
@ -289,6 +382,6 @@ class EnvManager:
|
||||||
volume_mounts.append(f"{path}:/app/documents:Z")
|
volume_mounts.append(f"{path}:/app/documents:Z")
|
||||||
else:
|
else:
|
||||||
# Additional paths map to numbered directories
|
# Additional paths map to numbered directories
|
||||||
volume_mounts.append(f"{path}:/app/documents{i+1}:Z")
|
volume_mounts.append(f"{path}:/app/documents{i + 1}:Z")
|
||||||
|
|
||||||
return volume_mounts
|
return volume_mounts
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue