diff --git a/application/agents/llm_handler.py b/application/agents/llm_handler.py index 9267dc53..a70357f8 100644 --- a/application/agents/llm_handler.py +++ b/application/agents/llm_handler.py @@ -72,9 +72,9 @@ class OpenAILLMHandler(LLMHandler): while True: tool_calls = {} for chunk in resp: - if isinstance(chunk, str): + if isinstance(chunk, str) and len(chunk) > 0: return - else: + elif hasattr(chunk, "delta"): chunk_delta = chunk.delta if ( @@ -113,6 +113,8 @@ class OpenAILLMHandler(LLMHandler): tool_response, call_id = agent._execute_tool_action( tools_dict, call ) + if isinstance(call["function"]["arguments"], str): + call["function"]["arguments"] = json.loads(call["function"]["arguments"]) function_call_dict = { "function_call": { @@ -156,6 +158,8 @@ class OpenAILLMHandler(LLMHandler): and chunk.finish_reason == "stop" ): return + elif isinstance(chunk, str) and len(chunk) == 0: + continue resp = agent.llm.gen_stream( model=agent.gpt_model, messages=messages, tools=agent.tools diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index ce06fda5..16d2c481 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -42,6 +42,8 @@ elif settings.LLM_NAME == "anthropic": gpt_model = "claude-2" elif settings.LLM_NAME == "groq": gpt_model = "llama3-8b-8192" +elif settings.LLM_NAME == "novita": + gpt_model = "deepseek/deepseek-r1" if settings.MODEL_NAME: # in case there is particular model name configured gpt_model = settings.MODEL_NAME @@ -733,7 +735,6 @@ class Search(Resource): retriever = RetrieverCreator.create_retriever( retriever_name, - question=question, source=source, chat_history=[], prompt="default", @@ -743,7 +744,7 @@ class Search(Resource): user_api_key=user_api_key, ) - docs = retriever.search() + docs = retriever.search(question) retriever_params = retriever.get_params() user_logs_collection.insert_one( diff --git a/application/cache.py b/application/cache.py index 80dee4f4..117b444a 100644 --- a/application/cache.py +++ b/application/cache.py @@ -11,21 +11,25 @@ from application.utils import get_hash logger = logging.getLogger(__name__) _redis_instance = None +_redis_creation_failed = False _instance_lock = Lock() - def get_redis_instance(): - global _redis_instance - if _redis_instance is None: + global _redis_instance, _redis_creation_failed + if _redis_instance is None and not _redis_creation_failed: with _instance_lock: - if _redis_instance is None: + if _redis_instance is None and not _redis_creation_failed: try: _redis_instance = redis.Redis.from_url( settings.CACHE_REDIS_URL, socket_connect_timeout=2 ) + except ValueError as e: + logger.error(f"Invalid Redis URL: {e}") + _redis_creation_failed = True # Stop future attempts + _redis_instance = None except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") - _redis_instance = None + _redis_instance = None # Keep trying for connection errors return _redis_instance @@ -41,36 +45,48 @@ def gen_cache_key(messages, model="docgpt", tools=None): def gen_cache(func): def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): + if tools is not None: + return func(self, model, messages, stream, tools, *args, **kwargs) + try: cache_key = gen_cache_key(messages, model, tools) - redis_client = get_redis_instance() - if redis_client: - try: - cached_response = redis_client.get(cache_key) - if cached_response: - return cached_response.decode("utf-8") - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") - - result = func(self, model, messages, stream, tools, *args, **kwargs) - if redis_client and isinstance(result, str): - try: - redis_client.set(cache_key, result, ex=1800) - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") - - return result except ValueError as e: - logger.error(e) - return "Error: No user message found in the conversation to generate a cache key." + logger.error(f"Cache key generation failed: {e}") + return func(self, model, messages, stream, tools, *args, **kwargs) + + redis_client = get_redis_instance() + if redis_client: + try: + cached_response = redis_client.get(cache_key) + if cached_response: + return cached_response.decode("utf-8") + except Exception as e: + logger.error(f"Error getting cached response: {e}") + + result = func(self, model, messages, stream, tools, *args, **kwargs) + if redis_client and isinstance(result, str): + try: + redis_client.set(cache_key, result, ex=1800) + except Exception as e: + logger.error(f"Error setting cache: {e}") + + return result return wrapper def stream_cache(func): def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): - cache_key = gen_cache_key(messages, model, tools) - logger.info(f"Stream cache key: {cache_key}") + if tools is not None: + yield from func(self, model, messages, stream, tools, *args, **kwargs) + return + + try: + cache_key = gen_cache_key(messages, model, tools) + except ValueError as e: + logger.error(f"Cache key generation failed: {e}") + yield from func(self, model, messages, stream, tools, *args, **kwargs) + return redis_client = get_redis_instance() if redis_client: @@ -81,23 +97,21 @@ def stream_cache(func): cached_response = json.loads(cached_response.decode("utf-8")) for chunk in cached_response: yield chunk - time.sleep(0.03) + time.sleep(0.03) # Simulate streaming delay return - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") + except Exception as e: + logger.error(f"Error getting cached stream: {e}") - result = func(self, model, messages, stream, tools=tools, *args, **kwargs) stream_cache_data = [] - - for chunk in result: - stream_cache_data.append(chunk) + for chunk in func(self, model, messages, stream, tools, *args, **kwargs): yield chunk + stream_cache_data.append(str(chunk)) if redis_client: try: redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800) logger.info(f"Stream cache saved for key: {cache_key}") - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") + except Exception as e: + logger.error(f"Error setting stream cache: {e}") return wrapper diff --git a/application/llm/docsgpt_provider.py b/application/llm/docsgpt_provider.py index bb23d824..001035c4 100644 --- a/application/llm/docsgpt_provider.py +++ b/application/llm/docsgpt_provider.py @@ -1,34 +1,131 @@ -from application.llm.base import BaseLLM import json -import requests + +from application.core.settings import settings +from application.llm.base import BaseLLM class DocsGPTAPILLM(BaseLLM): def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): + from openai import OpenAI + super().__init__(*args, **kwargs) - self.api_key = api_key + self.client = OpenAI(api_key="sk-docsgpt-public", base_url="https://oai.arc53.com") self.user_api_key = user_api_key - self.endpoint = "https://llm.arc53.com" + self.api_key = api_key - def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs): - response = requests.post( - f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30} - ) - response_clean = response.json()["a"].replace("###", "") + def _clean_messages_openai(self, messages): + cleaned_messages = [] + for message in messages: + role = message.get("role") + content = message.get("content") - return response_clean + if role == "model": + role = "assistant" - def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs): - response = requests.post( - f"{self.endpoint}/stream", - json={"messages": messages, "max_new_tokens": 256}, - stream=True, - ) + if role and content is not None: + if isinstance(content, str): + cleaned_messages.append({"role": role, "content": content}) + elif isinstance(content, list): + for item in content: + if "text" in item: + cleaned_messages.append( + {"role": role, "content": item["text"]} + ) + elif "function_call" in item: + tool_call = { + "id": item["function_call"]["call_id"], + "type": "function", + "function": { + "name": item["function_call"]["name"], + "arguments": json.dumps( + item["function_call"]["args"] + ), + }, + } + cleaned_messages.append( + { + "role": "assistant", + "content": None, + "tool_calls": [tool_call], + } + ) + elif "function_response" in item: + cleaned_messages.append( + { + "role": "tool", + "tool_call_id": item["function_response"][ + "call_id" + ], + "content": json.dumps( + item["function_response"]["response"]["result"] + ), + } + ) + else: + raise ValueError( + f"Unexpected content dictionary format: {item}" + ) + else: + raise ValueError(f"Unexpected content type: {type(content)}") - for line in response.iter_lines(): - if line: - data_str = line.decode("utf-8") - if data_str.startswith("data: "): - data = json.loads(data_str[6:]) - yield data["a"] + return cleaned_messages + + def _raw_gen( + self, + baseself, + model, + messages, + stream=False, + tools=None, + engine=settings.AZURE_DEPLOYMENT_NAME, + **kwargs, + ): + messages = self._clean_messages_openai(messages) + if tools: + response = self.client.chat.completions.create( + model="docsgpt", + messages=messages, + stream=stream, + tools=tools, + **kwargs, + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model="docsgpt", messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content + + def _raw_gen_stream( + self, + baseself, + model, + messages, + stream=True, + tools=None, + engine=settings.AZURE_DEPLOYMENT_NAME, + **kwargs, + ): + messages = self._clean_messages_openai(messages) + if tools: + response = self.client.chat.completions.create( + model="docsgpt", + messages=messages, + stream=stream, + tools=tools, + **kwargs, + ) + else: + response = self.client.chat.completions.create( + model="docsgpt", messages=messages, stream=stream, **kwargs + ) + + for line in response: + if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0: + yield line.choices[0].delta.content + elif len(line.choices) > 0: + yield line.choices[0] + + def _supports_tools(self): + return True \ No newline at end of file diff --git a/application/llm/llm_creator.py b/application/llm/llm_creator.py index f32089de..9f1305ba 100644 --- a/application/llm/llm_creator.py +++ b/application/llm/llm_creator.py @@ -7,7 +7,7 @@ from application.llm.anthropic import AnthropicLLM from application.llm.docsgpt_provider import DocsGPTAPILLM from application.llm.premai import PremAILLM from application.llm.google_ai import GoogleLLM - +from application.llm.novita import NovitaLLM class LLMCreator: llms = { @@ -20,7 +20,8 @@ class LLMCreator: "docsgpt": DocsGPTAPILLM, "premai": PremAILLM, "groq": GroqLLM, - "google": GoogleLLM + "google": GoogleLLM, + "novita": NovitaLLM } @classmethod diff --git a/application/llm/novita.py b/application/llm/novita.py new file mode 100644 index 00000000..8d6ac042 --- /dev/null +++ b/application/llm/novita.py @@ -0,0 +1,32 @@ +from application.llm.base import BaseLLM +from openai import OpenAI + + +class NovitaLLM(BaseLLM): + def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.client = OpenAI(api_key=api_key, base_url="https://api.novita.ai/v3/openai") + self.api_key = api_key + self.user_api_key = user_api_key + + def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs): + if tools: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, tools=tools, **kwargs + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content + + def _raw_gen_stream( + self, baseself, model, messages, stream=True, tools=None, **kwargs + ): + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + for line in response: + if line.choices[0].delta.content is not None: + yield line.choices[0].delta.content diff --git a/application/llm/openai.py b/application/llm/openai.py index 938de523..5e11a072 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -125,9 +125,9 @@ class OpenAILLM(BaseLLM): ) for line in response: - if line.choices[0].delta.content is not None: + if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0: yield line.choices[0].delta.content - else: + elif len(line.choices) > 0: yield line.choices[0] def _supports_tools(self): @@ -137,17 +137,17 @@ class OpenAILLM(BaseLLM): class AzureOpenAILLM(OpenAILLM): def __init__( - self, openai_api_key, openai_api_base, openai_api_version, deployment_name + self, api_key, user_api_key, *args, **kwargs ): - super().__init__(openai_api_key) + + super().__init__(api_key) self.api_base = (settings.OPENAI_API_BASE,) self.api_version = (settings.OPENAI_API_VERSION,) self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,) from openai import AzureOpenAI self.client = AzureOpenAI( - api_key=openai_api_key, + api_key=api_key, api_version=settings.OPENAI_API_VERSION, - api_base=settings.OPENAI_API_BASE, - deployment_name=settings.AZURE_DEPLOYMENT_NAME, + azure_endpoint=settings.OPENAI_API_BASE ) diff --git a/application/requirements.txt b/application/requirements.txt index c00c24f7..5323fe85 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -4,7 +4,7 @@ beautifulsoup4==4.12.3 celery==5.4.0 dataclasses-json==0.6.7 docx2txt==0.8 -duckduckgo-search==7.4.2 +duckduckgo-search==7.5.2 ebooklib==0.18 elastic-transport==8.17.0 elasticsearch==8.17.1 @@ -30,12 +30,12 @@ jsonschema==4.23.0 jsonschema-spec==0.2.4 jsonschema-specifications==2023.7.1 kombu==5.4.2 -langchain==0.3.14 -langchain-community==0.3.14 -langchain-core==0.3.40 -langchain-openai==0.3.0 -langchain-text-splitters==0.3.5 -langsmith==0.2.10 +langchain==0.3.20 +langchain-community==0.3.19 +langchain-core==0.3.45 +langchain-openai==0.3.8 +langchain-text-splitters==0.3.6 +langsmith==0.3.15 lazy-object-proxy==1.10.0 lxml==5.3.1 markupsafe==3.0.2 @@ -45,7 +45,7 @@ multidict==6.1.0 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 -openai==1.59.5 +openai==1.66.3 openapi-schema-validator==0.6.3 openapi-spec-validator==0.6.0 openapi3-parser==1.1.19 diff --git a/frontend/src/Hero.tsx b/frontend/src/Hero.tsx index a000ab2e..0161eac2 100644 --- a/frontend/src/Hero.tsx +++ b/frontend/src/Hero.tsx @@ -1,6 +1,6 @@ -import { Fragment } from 'react'; import DocsGPT3 from './assets/cute_docsgpt3.svg'; import { useTranslation } from 'react-i18next'; + export default function Hero({ handleQuestion, }: { @@ -17,38 +17,41 @@ export default function Hero({ header: string; query: string; }>; - return ( -
{t('settings.label')}
diff --git a/frontend/src/assets/eye-view.svg b/frontend/src/assets/eye-view.svg
new file mode 100644
index 00000000..1e569cfd
--- /dev/null
+++ b/frontend/src/assets/eye-view.svg
@@ -0,0 +1,11 @@
+
diff --git a/frontend/src/assets/sync.svg b/frontend/src/assets/sync.svg
index 003dec43..4733bbdb 100644
--- a/frontend/src/assets/sync.svg
+++ b/frontend/src/assets/sync.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/src/assets/user.png b/frontend/src/assets/user.png
deleted file mode 100644
index 3cc5d2f1..00000000
Binary files a/frontend/src/assets/user.png and /dev/null differ
diff --git a/frontend/src/assets/user.svg b/frontend/src/assets/user.svg
new file mode 100644
index 00000000..970bffb5
--- /dev/null
+++ b/frontend/src/assets/user.svg
@@ -0,0 +1,27 @@
+
+
+
diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx
new file mode 100644
index 00000000..96497482
--- /dev/null
+++ b/frontend/src/components/ContextMenu.tsx
@@ -0,0 +1,132 @@
+import { SyntheticEvent, useRef, useEffect, CSSProperties } from 'react';
+
+export interface MenuOption {
+ icon?: string;
+ label: string;
+ onClick: (event: SyntheticEvent) => void;
+ variant?: 'primary' | 'danger';
+ iconClassName?: string;
+ iconWidth?: number;
+ iconHeight?: number;
+}
+
+interface ContextMenuProps {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+ options: MenuOption[];
+ anchorRef: React.RefObject
+ ) : (
+