mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-20 02:16:39 +00:00
Merge branch 'main' of https://github.com/arc53/DocsGPT
This commit is contained in:
commit
a6eedc6d84
24 files changed
+980
-309
No files matched your search
@@ -0,0 +1,14 @@
|
|||||||
|
from application.agents.classic_agent import ClassicAgent
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCreator:
|
||||||
|
agents = {
|
||||||
|
"classic": ClassicAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_agent(cls, type, *args, **kwargs):
|
||||||
|
agent_class = cls.agents.get(type.lower())
|
||||||
|
if not agent_class:
|
||||||
|
raise ValueError(f"No agent class found for type {type}")
|
||||||
|
return agent_class(*args, **kwargs)
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
|
from typing import Dict, Generator
|
||||||
|
|
||||||
|
from application.agents.llm_handler import get_llm_handler
|
||||||
|
from application.agents.tools.tool_action_parser import ToolActionParser
|
||||||
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
|
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.llm.llm_creator import LLMCreator
|
from application.llm.llm_creator import LLMCreator
|
||||||
from application.tools.llm_handler import get_llm_handler
|
|
||||||
from application.tools.tool_action_parser import ToolActionParser
|
|
||||||
from application.tools.tool_manager import ToolManager
|
|
||||||
|
|
||||||
|
|
||||||
class Agent:
|
class BaseAgent:
|
||||||
def __init__(self, llm_name, gpt_model, api_key, user_api_key=None):
|
def __init__(self, endpoint, llm_name, gpt_model, api_key, user_api_key=None):
|
||||||
# Initialize the LLM with the provided parameters
|
self.endpoint = endpoint
|
||||||
self.llm = LLMCreator.create_llm(
|
self.llm = LLMCreator.create_llm(
|
||||||
llm_name, api_key=api_key, user_api_key=user_api_key
|
llm_name, api_key=api_key, user_api_key=user_api_key
|
||||||
)
|
)
|
||||||
self.llm_handler = get_llm_handler(llm_name)
|
self.llm_handler = get_llm_handler(llm_name)
|
||||||
self.gpt_model = gpt_model
|
self.gpt_model = gpt_model
|
||||||
# Static tool configuration (to be replaced later)
|
|
||||||
self.tools = []
|
self.tools = []
|
||||||
self.tool_config = {}
|
self.tool_config = {}
|
||||||
self.tool_calls = []
|
self.tool_calls = []
|
||||||
|
|
||||||
|
def gen(self, *args, **kwargs) -> Generator[Dict, None, None]:
|
||||||
|
raise NotImplementedError('Method "gen" must be implemented in the child class')
|
||||||
|
|
||||||
def _get_user_tools(self, user="local"):
|
def _get_user_tools(self, user="local"):
|
||||||
mongo = MongoDB.get_client()
|
mongo = MongoDB.get_client()
|
||||||
db = mongo["docsgpt"]
|
db = mongo["docsgpt"]
|
||||||
@@ -135,50 +140,3 @@ class Agent:
|
|||||||
self.tool_calls.append(tool_call_data)
|
self.tool_calls.append(tool_call_data)
|
||||||
|
|
||||||
return result, call_id
|
return result, call_id
|
||||||
|
|
||||||
def _simple_tool_agent(self, messages):
|
|
||||||
tools_dict = self._get_user_tools()
|
|
||||||
self._prepare_tools(tools_dict)
|
|
||||||
|
|
||||||
resp = self.llm.gen(model=self.gpt_model, messages=messages, tools=self.tools)
|
|
||||||
|
|
||||||
if isinstance(resp, str):
|
|
||||||
yield resp
|
|
||||||
return
|
|
||||||
if (
|
|
||||||
hasattr(resp, "message")
|
|
||||||
and hasattr(resp.message, "content")
|
|
||||||
and resp.message.content is not None
|
|
||||||
):
|
|
||||||
yield resp.message.content
|
|
||||||
return
|
|
||||||
|
|
||||||
resp = self.llm_handler.handle_response(self, resp, tools_dict, messages)
|
|
||||||
|
|
||||||
if isinstance(resp, str):
|
|
||||||
yield resp
|
|
||||||
elif (
|
|
||||||
hasattr(resp, "message")
|
|
||||||
and hasattr(resp.message, "content")
|
|
||||||
and resp.message.content is not None
|
|
||||||
):
|
|
||||||
yield resp.message.content
|
|
||||||
else:
|
|
||||||
completion = self.llm.gen_stream(
|
|
||||||
model=self.gpt_model, messages=messages, tools=self.tools
|
|
||||||
)
|
|
||||||
for line in completion:
|
|
||||||
yield line
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def gen(self, messages):
|
|
||||||
self.tool_calls = []
|
|
||||||
if self.llm.supports_tools():
|
|
||||||
resp = self._simple_tool_agent(messages)
|
|
||||||
for line in resp:
|
|
||||||
yield line
|
|
||||||
else:
|
|
||||||
resp = self.llm.gen_stream(model=self.gpt_model, messages=messages)
|
|
||||||
for line in resp:
|
|
||||||
yield line
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Dict, Generator
|
||||||
|
|
||||||
|
from application.agents.base import BaseAgent
|
||||||
|
from application.logging import build_stack_data, log_activity, LogContext
|
||||||
|
|
||||||
|
from application.retriever.base import BaseRetriever
|
||||||
|
|
||||||
|
|
||||||
|
class ClassicAgent(BaseAgent):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
endpoint,
|
||||||
|
llm_name,
|
||||||
|
gpt_model,
|
||||||
|
api_key,
|
||||||
|
user_api_key=None,
|
||||||
|
prompt="",
|
||||||
|
chat_history=None,
|
||||||
|
):
|
||||||
|
super().__init__(endpoint, llm_name, gpt_model, api_key, user_api_key)
|
||||||
|
self.prompt = prompt
|
||||||
|
self.chat_history = chat_history if chat_history is not None else []
|
||||||
|
|
||||||
|
@log_activity()
|
||||||
|
def gen(
|
||||||
|
self, query: str, retriever: BaseRetriever, log_context: LogContext = None
|
||||||
|
) -> Generator[Dict, None, None]:
|
||||||
|
yield from self._gen_inner(query, retriever, log_context)
|
||||||
|
|
||||||
|
def _gen_inner(
|
||||||
|
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||||
|
) -> Generator[Dict, None, None]:
|
||||||
|
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||||
|
|
||||||
|
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
|
||||||
|
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
|
||||||
|
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||||
|
|
||||||
|
if len(self.chat_history) > 0:
|
||||||
|
for i in self.chat_history:
|
||||||
|
if "prompt" in i and "response" in i:
|
||||||
|
messages_combine.append({"role": "user", "content": i["prompt"]})
|
||||||
|
messages_combine.append(
|
||||||
|
{"role": "assistant", "content": i["response"]}
|
||||||
|
)
|
||||||
|
if "tool_calls" in i:
|
||||||
|
for tool_call in i["tool_calls"]:
|
||||||
|
call_id = tool_call.get("call_id")
|
||||||
|
if call_id is None or call_id == "None":
|
||||||
|
call_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
function_call_dict = {
|
||||||
|
"function_call": {
|
||||||
|
"name": tool_call.get("action_name"),
|
||||||
|
"args": tool_call.get("arguments"),
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function_response_dict = {
|
||||||
|
"function_response": {
|
||||||
|
"name": tool_call.get("action_name"),
|
||||||
|
"response": {"result": tool_call.get("result")},
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages_combine.append(
|
||||||
|
{"role": "assistant", "content": [function_call_dict]}
|
||||||
|
)
|
||||||
|
messages_combine.append(
|
||||||
|
{"role": "tool", "content": [function_response_dict]}
|
||||||
|
)
|
||||||
|
messages_combine.append({"role": "user", "content": query})
|
||||||
|
|
||||||
|
tools_dict = self._get_user_tools()
|
||||||
|
self._prepare_tools(tools_dict)
|
||||||
|
|
||||||
|
resp = self._llm_gen(messages_combine, log_context)
|
||||||
|
|
||||||
|
if isinstance(resp, str):
|
||||||
|
yield {"answer": resp}
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
hasattr(resp, "message")
|
||||||
|
and hasattr(resp.message, "content")
|
||||||
|
and resp.message.content is not None
|
||||||
|
):
|
||||||
|
yield {"answer": resp.message.content}
|
||||||
|
return
|
||||||
|
|
||||||
|
resp = self._llm_handler(resp, tools_dict, messages_combine, log_context)
|
||||||
|
|
||||||
|
if isinstance(resp, str):
|
||||||
|
yield {"answer": resp}
|
||||||
|
elif (
|
||||||
|
hasattr(resp, "message")
|
||||||
|
and hasattr(resp.message, "content")
|
||||||
|
and resp.message.content is not None
|
||||||
|
):
|
||||||
|
yield {"answer": resp.message.content}
|
||||||
|
else:
|
||||||
|
completion = self.llm.gen_stream(
|
||||||
|
model=self.gpt_model, messages=messages_combine, tools=self.tools
|
||||||
|
)
|
||||||
|
for line in completion:
|
||||||
|
if isinstance(line, str):
|
||||||
|
yield {"answer": line}
|
||||||
|
|
||||||
|
yield {"tool_calls": self.tool_calls.copy()}
|
||||||
|
|
||||||
|
def _retriever_search(self, retriever, query, log_context):
|
||||||
|
retrieved_data = retriever.search(query)
|
||||||
|
if log_context:
|
||||||
|
data = build_stack_data(retriever, exclude_attributes=["llm"])
|
||||||
|
log_context.stacks.append({"component": "retriever", "data": data})
|
||||||
|
return retrieved_data
|
||||||
|
|
||||||
|
def _llm_gen(self, messages_combine, log_context):
|
||||||
|
resp = self.llm.gen_stream(
|
||||||
|
model=self.gpt_model, messages=messages_combine, tools=self.tools
|
||||||
|
)
|
||||||
|
if log_context:
|
||||||
|
data = build_stack_data(self.llm)
|
||||||
|
log_context.stacks.append({"component": "llm", "data": data})
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def _llm_handler(self, resp, tools_dict, messages_combine, log_context):
|
||||||
|
resp = self.llm_handler.handle_response(
|
||||||
|
self, resp, tools_dict, messages_combine
|
||||||
|
)
|
||||||
|
if log_context:
|
||||||
|
data = build_stack_data(self.llm_handler)
|
||||||
|
log_context.stacks.append({"component": "llm_handler", "data": data})
|
||||||
|
return resp
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from application.logging import build_stack_data
|
||||||
|
|
||||||
|
|
||||||
|
class LLMHandler(ABC):
|
||||||
|
def __init__(self):
|
||||||
|
self.llm_calls = []
|
||||||
|
self.tool_calls = []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle_response(self, agent, resp, tools_dict, messages, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAILLMHandler(LLMHandler):
|
||||||
|
def handle_response(self, agent, resp, tools_dict, messages, stream: bool = True):
|
||||||
|
if not stream:
|
||||||
|
while hasattr(resp, "finish_reason") and resp.finish_reason == "tool_calls":
|
||||||
|
message = json.loads(resp.model_dump_json())["message"]
|
||||||
|
keys_to_remove = {"audio", "function_call", "refusal"}
|
||||||
|
filtered_data = {
|
||||||
|
k: v for k, v in message.items() if k not in keys_to_remove
|
||||||
|
}
|
||||||
|
messages.append(filtered_data)
|
||||||
|
|
||||||
|
tool_calls = resp.message.tool_calls
|
||||||
|
for call in tool_calls:
|
||||||
|
try:
|
||||||
|
self.tool_calls.append(call)
|
||||||
|
tool_response, call_id = agent._execute_tool_action(
|
||||||
|
tools_dict, call
|
||||||
|
)
|
||||||
|
function_call_dict = {
|
||||||
|
"function_call": {
|
||||||
|
"name": call.function.name,
|
||||||
|
"args": call.function.arguments,
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function_response_dict = {
|
||||||
|
"function_response": {
|
||||||
|
"name": call.function.name,
|
||||||
|
"response": {"result": tool_response},
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
{"role": "assistant", "content": [function_call_dict]}
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
{"role": "tool", "content": [function_response_dict]}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": f"Error executing tool: {str(e)}",
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = agent.llm.gen_stream(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
tool_calls = {}
|
||||||
|
for chunk in resp:
|
||||||
|
if isinstance(chunk, str):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
chunk_delta = chunk.delta
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(chunk_delta, "tool_calls")
|
||||||
|
and chunk_delta.tool_calls is not None
|
||||||
|
):
|
||||||
|
for tool_call in chunk_delta.tool_calls:
|
||||||
|
index = tool_call.index
|
||||||
|
if index not in tool_calls:
|
||||||
|
tool_calls[index] = {
|
||||||
|
"id": "",
|
||||||
|
"function": {"name": "", "arguments": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
current = tool_calls[index]
|
||||||
|
if tool_call.id:
|
||||||
|
current["id"] = tool_call.id
|
||||||
|
if tool_call.function.name:
|
||||||
|
current["function"][
|
||||||
|
"name"
|
||||||
|
] = tool_call.function.name
|
||||||
|
if tool_call.function.arguments:
|
||||||
|
current["function"][
|
||||||
|
"arguments"
|
||||||
|
] += tool_call.function.arguments
|
||||||
|
tool_calls[index] = current
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(chunk, "finish_reason")
|
||||||
|
and chunk.finish_reason == "tool_calls"
|
||||||
|
):
|
||||||
|
for index in sorted(tool_calls.keys()):
|
||||||
|
call = tool_calls[index]
|
||||||
|
try:
|
||||||
|
self.tool_calls.append(call)
|
||||||
|
tool_response, call_id = agent._execute_tool_action(
|
||||||
|
tools_dict, call
|
||||||
|
)
|
||||||
|
|
||||||
|
function_call_dict = {
|
||||||
|
"function_call": {
|
||||||
|
"name": call["function"]["name"],
|
||||||
|
"args": call["function"]["arguments"],
|
||||||
|
"call_id": call["id"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function_response_dict = {
|
||||||
|
"function_response": {
|
||||||
|
"name": call["function"]["name"],
|
||||||
|
"response": {"result": tool_response},
|
||||||
|
"call_id": call["id"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [function_call_dict],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": [function_response_dict],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": f"Error executing tool: {str(e)}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tool_calls = {}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(chunk, "finish_reason")
|
||||||
|
and chunk.finish_reason == "stop"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
resp = agent.llm.gen_stream(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleLLMHandler(LLMHandler):
|
||||||
|
def handle_response(self, agent, resp, tools_dict, messages, stream: bool = True):
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not stream:
|
||||||
|
response = agent.llm.gen(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
if response.candidates and response.candidates[0].content.parts:
|
||||||
|
tool_call_found = False
|
||||||
|
for part in response.candidates[0].content.parts:
|
||||||
|
if part.function_call:
|
||||||
|
tool_call_found = True
|
||||||
|
self.tool_calls.append(part.function_call)
|
||||||
|
tool_response, call_id = agent._execute_tool_action(
|
||||||
|
tools_dict, part.function_call
|
||||||
|
)
|
||||||
|
function_response_part = types.Part.from_function_response(
|
||||||
|
name=part.function_call.name,
|
||||||
|
response={"result": tool_response},
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
{"role": "model", "content": [part.to_json_dict()]}
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": [function_response_part.to_json_dict()],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not tool_call_found
|
||||||
|
and response.candidates[0].content.parts
|
||||||
|
and response.candidates[0].content.parts[0].text
|
||||||
|
):
|
||||||
|
return response.candidates[0].content.parts[0].text
|
||||||
|
elif not tool_call_found:
|
||||||
|
return response.candidates[0].content.parts
|
||||||
|
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
|
else:
|
||||||
|
response = agent.llm.gen_stream(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
|
||||||
|
tool_call_found = False
|
||||||
|
for result in response:
|
||||||
|
if hasattr(result, "function_call"):
|
||||||
|
tool_call_found = True
|
||||||
|
self.tool_calls.append(result.function_call)
|
||||||
|
tool_response, call_id = agent._execute_tool_action(
|
||||||
|
tools_dict, result.function_call
|
||||||
|
)
|
||||||
|
function_response_part = types.Part.from_function_response(
|
||||||
|
name=result.function_call.name,
|
||||||
|
response={"result": tool_response},
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
{"role": "model", "content": [result.to_json_dict()]}
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": [function_response_part.to_json_dict()],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not tool_call_found:
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_handler(llm_type):
|
||||||
|
handlers = {
|
||||||
|
"openai": OpenAILLMHandler(),
|
||||||
|
"google": GoogleLLMHandler(),
|
||||||
|
}
|
||||||
|
return handlers.get(llm_type, OpenAILLMHandler())
|
||||||
+21
-4
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from application.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
class APITool(Tool):
|
class APITool(Tool):
|
||||||
@@ -31,10 +31,27 @@ class APITool(Tool):
|
|||||||
print(f"Making API call: {method} {url} with body: {body}")
|
print(f"Making API call: {method} {url} with body: {body}")
|
||||||
response = requests.request(method, url, headers=headers, data=body)
|
response = requests.request(method, url, headers=headers, data=body)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
try:
|
|
||||||
data = response.json()
|
content_type = response.headers.get(
|
||||||
except ValueError:
|
"Content-Type", "application/json"
|
||||||
|
).lower()
|
||||||
|
if "application/json" in content_type:
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Error decoding JSON: {e}. Raw response: {response.text}")
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": f"API call returned invalid JSON. Error: {e}",
|
||||||
|
"data": response.text,
|
||||||
|
}
|
||||||
|
elif "text/" in content_type or "application/xml" in content_type:
|
||||||
|
data = response.text
|
||||||
|
elif not response.content:
|
||||||
data = None
|
data = None
|
||||||
|
else:
|
||||||
|
print(f"Unsupported content type: {content_type}")
|
||||||
|
data = response.content
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
File renamed without changes.
@@ -0,0 +1,217 @@
|
|||||||
|
import requests
|
||||||
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
|
class BraveSearchTool(Tool):
|
||||||
|
"""
|
||||||
|
Brave Search
|
||||||
|
A tool for performing web and image searches using the Brave Search API.
|
||||||
|
Requires an API key for authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.token = config.get("token", "")
|
||||||
|
self.base_url = "https://api.search.brave.com/res/v1"
|
||||||
|
|
||||||
|
def execute_action(self, action_name, **kwargs):
|
||||||
|
actions = {
|
||||||
|
"brave_web_search": self._web_search,
|
||||||
|
"brave_image_search": self._image_search,
|
||||||
|
}
|
||||||
|
|
||||||
|
if action_name in actions:
|
||||||
|
return actions[action_name](**kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown action: {action_name}")
|
||||||
|
|
||||||
|
def _web_search(self, query, country="ALL", search_lang="en", count=10,
|
||||||
|
offset=0, safesearch="off", freshness=None,
|
||||||
|
result_filter=None, extra_snippets=False, summary=False):
|
||||||
|
"""
|
||||||
|
Performs a web search using the Brave Search API.
|
||||||
|
"""
|
||||||
|
print(f"Performing Brave web search for: {query}")
|
||||||
|
|
||||||
|
url = f"{self.base_url}/web/search"
|
||||||
|
|
||||||
|
# Build query parameters
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"country": country,
|
||||||
|
"search_lang": search_lang,
|
||||||
|
"count": min(count, 20),
|
||||||
|
"offset": min(offset, 9),
|
||||||
|
"safesearch": safesearch
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add optional parameters only if they have values
|
||||||
|
if freshness:
|
||||||
|
params["freshness"] = freshness
|
||||||
|
if result_filter:
|
||||||
|
params["result_filter"] = result_filter
|
||||||
|
if extra_snippets:
|
||||||
|
params["extra_snippets"] = 1
|
||||||
|
if summary:
|
||||||
|
params["summary"] = 1
|
||||||
|
|
||||||
|
# Set up headers
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"X-Subscription-Token": self.token
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
response = requests.get(url, params=params, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"results": response.json(),
|
||||||
|
"message": "Search completed successfully."
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": f"Search failed with status code: {response.status_code}."
|
||||||
|
}
|
||||||
|
|
||||||
|
def _image_search(self, query, country="ALL", search_lang="en", count=5,
|
||||||
|
safesearch="off", spellcheck=False):
|
||||||
|
"""
|
||||||
|
Performs an image search using the Brave Search API.
|
||||||
|
"""
|
||||||
|
print(f"Performing Brave image search for: {query}")
|
||||||
|
|
||||||
|
url = f"{self.base_url}/images/search"
|
||||||
|
|
||||||
|
# Build query parameters
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"country": country,
|
||||||
|
"search_lang": search_lang,
|
||||||
|
"count": min(count, 100), # API max is 100
|
||||||
|
"safesearch": safesearch,
|
||||||
|
"spellcheck": 1 if spellcheck else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up headers
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"X-Subscription-Token": self.token
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
response = requests.get(url, params=params, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"results": response.json(),
|
||||||
|
"message": "Image search completed successfully."
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": f"Image search failed with status code: {response.status_code}."
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_actions_metadata(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "brave_web_search",
|
||||||
|
"description": "Perform a web search using Brave Search",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The search query (max 400 characters, 50 words)",
|
||||||
|
},
|
||||||
|
# "country": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "The 2-character country code (default: US)",
|
||||||
|
# },
|
||||||
|
"search_lang": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The search language preference (default: en)",
|
||||||
|
},
|
||||||
|
# "count": {
|
||||||
|
# "type": "integer",
|
||||||
|
# "description": "Number of results to return (max 20, default: 10)",
|
||||||
|
# },
|
||||||
|
# "offset": {
|
||||||
|
# "type": "integer",
|
||||||
|
# "description": "Pagination offset (max 9, default: 0)",
|
||||||
|
# },
|
||||||
|
# "safesearch": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "Filter level for adult content (off, moderate, strict)",
|
||||||
|
# },
|
||||||
|
"freshness": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Time filter for results (pd: last 24h, pw: last week, pm: last month, py: last year)",
|
||||||
|
},
|
||||||
|
# "result_filter": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "Comma-delimited list of result types to include",
|
||||||
|
# },
|
||||||
|
# "extra_snippets": {
|
||||||
|
# "type": "boolean",
|
||||||
|
# "description": "Get additional excerpts from result pages",
|
||||||
|
# },
|
||||||
|
# "summary": {
|
||||||
|
# "type": "boolean",
|
||||||
|
# "description": "Enable summary generation in search results",
|
||||||
|
# }
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brave_image_search",
|
||||||
|
"description": "Perform an image search using Brave Search",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The search query (max 400 characters, 50 words)",
|
||||||
|
},
|
||||||
|
# "country": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "The 2-character country code (default: US)",
|
||||||
|
# },
|
||||||
|
# "search_lang": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "The search language preference (default: en)",
|
||||||
|
# },
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results to return (max 100, default: 5)",
|
||||||
|
},
|
||||||
|
# "safesearch": {
|
||||||
|
# "type": "string",
|
||||||
|
# "description": "Filter level for adult content (off, strict). Default: strict",
|
||||||
|
# },
|
||||||
|
# "spellcheck": {
|
||||||
|
# "type": "boolean",
|
||||||
|
# "description": "Whether to spellcheck provided query (default: true)",
|
||||||
|
# }
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_config_requirements(self):
|
||||||
|
return {
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brave Search API key for authentication"
|
||||||
|
},
|
||||||
|
}
|
||||||
+1
-2
@@ -1,5 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
from application.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
class CryptoPriceTool(Tool):
|
class CryptoPriceTool(Tool):
|
||||||
@@ -31,7 +31,6 @@ class CryptoPriceTool(Tool):
|
|||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
# data will be like {"USD": <price>} if the call is successful
|
|
||||||
if currency.upper() in data:
|
if currency.upper() in data:
|
||||||
return {
|
return {
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import psycopg2
|
import psycopg2
|
||||||
from application.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
class PostgresTool(Tool):
|
class PostgresTool(Tool):
|
||||||
"""
|
"""
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
from application.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
class TelegramTool(Tool):
|
class TelegramTool(Tool):
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolActionParser:
|
||||||
|
def __init__(self, llm_type):
|
||||||
|
self.llm_type = llm_type
|
||||||
|
self.parsers = {
|
||||||
|
"OpenAILLM": self._parse_openai_llm,
|
||||||
|
"GoogleLLM": self._parse_google_llm,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_args(self, call):
|
||||||
|
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
|
||||||
|
return parser(call)
|
||||||
|
|
||||||
|
def _parse_openai_llm(self, call):
|
||||||
|
if isinstance(call, dict):
|
||||||
|
try:
|
||||||
|
call_args = json.loads(call["function"]["arguments"])
|
||||||
|
tool_id = call["function"]["name"].split("_")[-1]
|
||||||
|
action_name = call["function"]["name"].rsplit("_", 1)[0]
|
||||||
|
except (KeyError, TypeError) as e:
|
||||||
|
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
||||||
|
return None, None, None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
call_args = json.loads(call.function.arguments)
|
||||||
|
tool_id = call.function.name.split("_")[-1]
|
||||||
|
action_name = call.function.name.rsplit("_", 1)[0]
|
||||||
|
except (AttributeError, TypeError) as e:
|
||||||
|
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
||||||
|
return None, None, None
|
||||||
|
return tool_id, action_name, call_args
|
||||||
|
|
||||||
|
def _parse_google_llm(self, call):
|
||||||
|
call_args = call.args
|
||||||
|
tool_id = call.name.split("_")[-1]
|
||||||
|
action_name = call.name.rsplit("_", 1)[0]
|
||||||
|
return tool_id, action_name, call_args
|
||||||
@@ -3,7 +3,7 @@ import inspect
|
|||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
from application.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
class ToolManager:
|
class ToolManager:
|
||||||
@@ -13,13 +13,11 @@ class ToolManager:
|
|||||||
self.load_tools()
|
self.load_tools()
|
||||||
|
|
||||||
def load_tools(self):
|
def load_tools(self):
|
||||||
tools_dir = os.path.join(os.path.dirname(__file__), "implementations")
|
tools_dir = os.path.join(os.path.dirname(__file__))
|
||||||
for finder, name, ispkg in pkgutil.iter_modules([tools_dir]):
|
for finder, name, ispkg in pkgutil.iter_modules([tools_dir]):
|
||||||
if name == "base" or name.startswith("__"):
|
if name == "base" or name.startswith("__"):
|
||||||
continue
|
continue
|
||||||
module = importlib.import_module(
|
module = importlib.import_module(f"application.agents.tools.{name}")
|
||||||
f"application.tools.implementations.{name}"
|
|
||||||
)
|
|
||||||
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if issubclass(obj, Tool) and obj is not Tool:
|
if issubclass(obj, Tool) and obj is not Tool:
|
||||||
tool_config = self.config.get(name, {})
|
tool_config = self.config.get(name, {})
|
||||||
@@ -27,9 +25,7 @@ class ToolManager:
|
|||||||
|
|
||||||
def load_tool(self, tool_name, tool_config):
|
def load_tool(self, tool_name, tool_config):
|
||||||
self.config[tool_name] = tool_config
|
self.config[tool_name] = tool_config
|
||||||
module = importlib.import_module(
|
module = importlib.import_module(f"application.agents.tools.{tool_name}")
|
||||||
f"application.tools.implementations.{tool_name}"
|
|
||||||
)
|
|
||||||
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if issubclass(obj, Tool) and obj is not Tool:
|
if issubclass(obj, Tool) and obj is not Tool:
|
||||||
return obj(tool_config)
|
return obj(tool_config)
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
|
||||||
|
|
||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
from flask import Blueprint, make_response, request, Response
|
from flask import Blueprint, make_response, request, Response
|
||||||
from flask_restx import fields, Namespace, Resource
|
from flask_restx import fields, Namespace, Resource
|
||||||
|
|
||||||
|
from application.agents.agent_creator import AgentCreator
|
||||||
|
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
@@ -206,6 +207,7 @@ def get_prompt(prompt_id):
|
|||||||
|
|
||||||
def complete_stream(
|
def complete_stream(
|
||||||
question,
|
question,
|
||||||
|
agent,
|
||||||
retriever,
|
retriever,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
user_api_key,
|
user_api_key,
|
||||||
@@ -217,8 +219,8 @@ def complete_stream(
|
|||||||
response_full = ""
|
response_full = ""
|
||||||
source_log_docs = []
|
source_log_docs = []
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
answer = retriever.gen()
|
answer = agent.gen(query=question, retriever=retriever)
|
||||||
sources = retriever.search()
|
sources = retriever.search(question)
|
||||||
for source in sources:
|
for source in sources:
|
||||||
if "text" in source:
|
if "text" in source:
|
||||||
source["text"] = source["text"][:100].strip() + "..."
|
source["text"] = source["text"][:100].strip() + "..."
|
||||||
@@ -384,9 +386,20 @@ class Stream(Resource):
|
|||||||
prompt = get_prompt(prompt_id)
|
prompt = get_prompt(prompt_id)
|
||||||
if "isNoneDoc" in data and data["isNoneDoc"] is True:
|
if "isNoneDoc" in data and data["isNoneDoc"] is True:
|
||||||
chunks = 0
|
chunks = 0
|
||||||
|
|
||||||
|
agent = AgentCreator.create_agent(
|
||||||
|
settings.AGENT_NAME,
|
||||||
|
endpoint="stream",
|
||||||
|
llm_name=settings.LLM_NAME,
|
||||||
|
gpt_model=gpt_model,
|
||||||
|
api_key=settings.API_KEY,
|
||||||
|
user_api_key=user_api_key,
|
||||||
|
prompt=prompt,
|
||||||
|
chat_history=history,
|
||||||
|
)
|
||||||
|
|
||||||
retriever = RetrieverCreator.create_retriever(
|
retriever = RetrieverCreator.create_retriever(
|
||||||
retriever_name,
|
retriever_name,
|
||||||
question=question,
|
|
||||||
source=source,
|
source=source,
|
||||||
chat_history=history,
|
chat_history=history,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
@@ -399,6 +412,7 @@ class Stream(Resource):
|
|||||||
return Response(
|
return Response(
|
||||||
complete_stream(
|
complete_stream(
|
||||||
question=question,
|
question=question,
|
||||||
|
agent=agent,
|
||||||
retriever=retriever,
|
retriever=retriever,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
|
||||||
|
|
||||||
from bson.binary import Binary, UuidRepresentation
|
from bson.binary import Binary, UuidRepresentation
|
||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
@@ -12,12 +12,13 @@ from flask import Blueprint, current_app, jsonify, make_response, redirect, requ
|
|||||||
from flask_restx import fields, inputs, Namespace, Resource
|
from flask_restx import fields, inputs, Namespace, Resource
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
|
|
||||||
from application.api.user.tasks import ingest, ingest_remote
|
from application.api.user.tasks import ingest, ingest_remote
|
||||||
|
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
from application.extensions import api
|
from application.extensions import api
|
||||||
from application.tools.tool_manager import ToolManager
|
|
||||||
from application.tts.google_tts import GoogleTTS
|
from application.tts.google_tts import GoogleTTS
|
||||||
from application.utils import check_required_fields, validate_function_name
|
from application.utils import check_required_fields, validate_function_name
|
||||||
from application.vectorstore.vector_creator import VectorCreator
|
from application.vectorstore.vector_creator import VectorCreator
|
||||||
@@ -449,22 +450,21 @@ class UploadRemote(Resource):
|
|||||||
return missing_fields
|
return missing_fields
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = json.loads(data["data"])
|
config = json.loads(data["data"])
|
||||||
source_data = None
|
source_data = None
|
||||||
|
|
||||||
if data["source"] == "github":
|
if data["source"] == "github":
|
||||||
source_data = config.get("repo_url")
|
source_data = config.get("repo_url")
|
||||||
elif data["source"] in ["crawler", "url"]:
|
elif data["source"] in ["crawler", "url"]:
|
||||||
source_data = config.get("url")
|
source_data = config.get("url")
|
||||||
elif data["source"] == "reddit":
|
elif data["source"] == "reddit":
|
||||||
source_data = config
|
source_data = config
|
||||||
|
|
||||||
|
task = ingest_remote.delay(
|
||||||
task = ingest_remote.delay(
|
|
||||||
source_data=source_data,
|
source_data=source_data,
|
||||||
job_name=data["name"],
|
job_name=data["name"],
|
||||||
user=data["user"],
|
user=data["user"],
|
||||||
loader=data["source"]
|
loader=data["source"],
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error uploading remote source: {err}")
|
current_app.logger.error(f"Error uploading remote source: {err}")
|
||||||
@@ -1932,11 +1932,14 @@ class UpdateTool(Resource):
|
|||||||
for action_name in list(data["config"]["actions"].keys()):
|
for action_name in list(data["config"]["actions"].keys()):
|
||||||
if not validate_function_name(action_name):
|
if not validate_function_name(action_name):
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify(
|
||||||
"success": False,
|
{
|
||||||
"message": f"Invalid function name '{action_name}'. Function names must match pattern '^[a-zA-Z0-9_-]+$'.",
|
"success": False,
|
||||||
"param": "tools[].function.name"
|
"message": f"Invalid function name '{action_name}'. Function names must match pattern '^[a-zA-Z0-9_-]+$'.",
|
||||||
}), 400
|
"param": "tools[].function.name",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
)
|
)
|
||||||
update_data["config"] = data["config"]
|
update_data["config"] = data["config"]
|
||||||
if "status" in data:
|
if "status" in data:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Settings(BaseSettings):
|
|||||||
"faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb"
|
"faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb"
|
||||||
)
|
)
|
||||||
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
||||||
|
AGENT_NAME: str = "classic"
|
||||||
|
|
||||||
# LLM Cache
|
# LLM Cache
|
||||||
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
||||||
|
|||||||
@@ -152,7 +152,15 @@ class GoogleLLM(BaseLLM):
|
|||||||
config=config,
|
config=config,
|
||||||
)
|
)
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
if chunk.text is not None:
|
if hasattr(chunk, "candidates") and chunk.candidates:
|
||||||
|
for candidate in chunk.candidates:
|
||||||
|
if candidate.content and candidate.content.parts:
|
||||||
|
for part in candidate.content.parts:
|
||||||
|
if part.function_call:
|
||||||
|
yield part
|
||||||
|
elif part.text:
|
||||||
|
yield part.text
|
||||||
|
elif hasattr(chunk, "text"):
|
||||||
yield chunk.text
|
yield chunk.text
|
||||||
|
|
||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
|
|||||||
@@ -111,13 +111,24 @@ class OpenAILLM(BaseLLM):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
messages = self._clean_messages_openai(messages)
|
messages = self._clean_messages_openai(messages)
|
||||||
response = self.client.chat.completions.create(
|
if tools:
|
||||||
model=model, messages=messages, stream=stream, **kwargs
|
response = self.client.chat.completions.create(
|
||||||
)
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
stream=stream,
|
||||||
|
tools=tools,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model=model, messages=messages, stream=stream, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
for line in response:
|
for line in response:
|
||||||
if line.choices[0].delta.content is not None:
|
if line.choices[0].delta.content is not None:
|
||||||
yield line.choices[0].delta.content
|
yield line.choices[0].delta.content
|
||||||
|
else:
|
||||||
|
yield line.choices[0]
|
||||||
|
|
||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import Any, Callable, Dict, Generator, List
|
||||||
|
|
||||||
|
from application.core.mongo_db import MongoDB
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LogContext:
|
||||||
|
def __init__(self, endpoint, activity_id, user, api_key, query):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.activity_id = activity_id
|
||||||
|
self.user = user
|
||||||
|
self.api_key = api_key
|
||||||
|
self.query = query
|
||||||
|
self.stacks = []
|
||||||
|
|
||||||
|
|
||||||
|
def build_stack_data(
|
||||||
|
obj: Any,
|
||||||
|
include_attributes: List[str] = None,
|
||||||
|
exclude_attributes: List[str] = None,
|
||||||
|
custom_data: Dict = None,
|
||||||
|
) -> Dict:
|
||||||
|
data = {}
|
||||||
|
if include_attributes is None:
|
||||||
|
include_attributes = []
|
||||||
|
for name, value in inspect.getmembers(obj):
|
||||||
|
if (
|
||||||
|
not name.startswith("_")
|
||||||
|
and not inspect.ismethod(value)
|
||||||
|
and not inspect.isfunction(value)
|
||||||
|
):
|
||||||
|
include_attributes.append(name)
|
||||||
|
for attr_name in include_attributes:
|
||||||
|
if exclude_attributes and attr_name in exclude_attributes:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
attr_value = getattr(obj, attr_name)
|
||||||
|
if attr_value is not None:
|
||||||
|
if isinstance(attr_value, (int, float, str, bool)):
|
||||||
|
data[attr_name] = attr_value
|
||||||
|
elif isinstance(attr_value, list):
|
||||||
|
if all(isinstance(item, dict) for item in attr_value):
|
||||||
|
data[attr_name] = attr_value
|
||||||
|
elif all(hasattr(item, "__dict__") for item in attr_value):
|
||||||
|
data[attr_name] = [item.__dict__ for item in attr_value]
|
||||||
|
else:
|
||||||
|
data[attr_name] = [str(item) for item in attr_value]
|
||||||
|
elif isinstance(attr_value, dict):
|
||||||
|
data[attr_name] = {k: str(v) for k, v in attr_value.items()}
|
||||||
|
else:
|
||||||
|
data[attr_name] = str(attr_value)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if custom_data:
|
||||||
|
data.update(custom_data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def log_activity() -> Callable:
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
activity_id = str(uuid.uuid4())
|
||||||
|
data = build_stack_data(args[0])
|
||||||
|
endpoint = data.get("endpoint", "")
|
||||||
|
user = data.get("user", "local")
|
||||||
|
api_key = data.get("user_api_key", "")
|
||||||
|
query = kwargs.get("query", getattr(args[0], "query", ""))
|
||||||
|
|
||||||
|
context = LogContext(endpoint, activity_id, user, api_key, query)
|
||||||
|
kwargs["log_context"] = context
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"Starting activity: {endpoint} - {activity_id} - User: {user}"
|
||||||
|
)
|
||||||
|
|
||||||
|
generator = func(*args, **kwargs)
|
||||||
|
yield from _consume_and_log(generator, context)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _consume_and_log(generator: Generator, context: "LogContext"):
|
||||||
|
try:
|
||||||
|
for item in generator:
|
||||||
|
yield item
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Error in {context.endpoint} - {context.activity_id}: {e}")
|
||||||
|
context.stacks.append({"component": "error", "data": {"message": str(e)}})
|
||||||
|
_log_to_mongodb(
|
||||||
|
endpoint=context.endpoint,
|
||||||
|
activity_id=context.activity_id,
|
||||||
|
user=context.user,
|
||||||
|
api_key=context.api_key,
|
||||||
|
query=context.query,
|
||||||
|
stacks=context.stacks,
|
||||||
|
level="error",
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
_log_to_mongodb(
|
||||||
|
endpoint=context.endpoint,
|
||||||
|
activity_id=context.activity_id,
|
||||||
|
user=context.user,
|
||||||
|
api_key=context.api_key,
|
||||||
|
query=context.query,
|
||||||
|
stacks=context.stacks,
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_to_mongodb(
|
||||||
|
endpoint: str,
|
||||||
|
activity_id: str,
|
||||||
|
user: str,
|
||||||
|
api_key: str,
|
||||||
|
query: str,
|
||||||
|
stacks: List[Dict],
|
||||||
|
level: str,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo["docsgpt"]
|
||||||
|
user_logs_collection = db["stack_logs"]
|
||||||
|
|
||||||
|
log_entry = {
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"id": activity_id,
|
||||||
|
"level": level,
|
||||||
|
"user": user,
|
||||||
|
"api_key": api_key,
|
||||||
|
"query": query,
|
||||||
|
"stacks": stacks,
|
||||||
|
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
}
|
||||||
|
user_logs_collection.insert_one(log_entry)
|
||||||
|
logging.debug(f"Logged activity to MongoDB: {activity_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to log to MongoDB: {e}")
|
||||||
@@ -1,28 +1,25 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
|
from application.llm.llm_creator import LLMCreator
|
||||||
from application.retriever.base import BaseRetriever
|
from application.retriever.base import BaseRetriever
|
||||||
from application.tools.agent import Agent
|
|
||||||
|
|
||||||
from application.vectorstore.vector_creator import VectorCreator
|
from application.vectorstore.vector_creator import VectorCreator
|
||||||
|
|
||||||
|
|
||||||
class ClassicRAG(BaseRetriever):
|
class ClassicRAG(BaseRetriever):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
question,
|
|
||||||
source,
|
source,
|
||||||
chat_history,
|
chat_history=None,
|
||||||
prompt,
|
prompt="",
|
||||||
chunks=2,
|
chunks=2,
|
||||||
token_limit=150,
|
token_limit=150,
|
||||||
gpt_model="docsgpt",
|
gpt_model="docsgpt",
|
||||||
user_api_key=None,
|
user_api_key=None,
|
||||||
|
llm_name=settings.LLM_NAME,
|
||||||
|
api_key=settings.API_KEY,
|
||||||
):
|
):
|
||||||
self.question = question
|
self.original_question = ""
|
||||||
self.vectorstore = source["active_docs"] if "active_docs" in source else None
|
self.chat_history = chat_history if chat_history is not None else []
|
||||||
self.chat_history = chat_history
|
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
self.chunks = chunks
|
self.chunks = chunks
|
||||||
self.gpt_model = gpt_model
|
self.gpt_model = gpt_model
|
||||||
@@ -37,12 +34,41 @@ class ClassicRAG(BaseRetriever):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.user_api_key = user_api_key
|
self.user_api_key = user_api_key
|
||||||
self.agent = Agent(
|
self.llm_name = llm_name
|
||||||
llm_name=settings.LLM_NAME,
|
self.api_key = api_key
|
||||||
gpt_model=self.gpt_model,
|
self.llm = LLMCreator.create_llm(
|
||||||
api_key=settings.API_KEY,
|
self.llm_name, api_key=self.api_key, user_api_key=self.user_api_key
|
||||||
user_api_key=self.user_api_key,
|
|
||||||
)
|
)
|
||||||
|
self.question = self._rephrase_query()
|
||||||
|
self.vectorstore = source["active_docs"] if "active_docs" in source else None
|
||||||
|
|
||||||
|
def _rephrase_query(self):
|
||||||
|
if (
|
||||||
|
not self.original_question
|
||||||
|
or not self.chat_history
|
||||||
|
or self.chat_history == []
|
||||||
|
):
|
||||||
|
return self.original_question
|
||||||
|
|
||||||
|
prompt = f"""Given the following conversation history:
|
||||||
|
{self.chat_history}
|
||||||
|
|
||||||
|
Rephrase the following user question to be a standalone search query
|
||||||
|
that captures all relevant context from the conversation:
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": prompt},
|
||||||
|
{"role": "user", "content": self.original_question},
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
rephrased_query = self.llm.gen(model=self.gpt_model, messages=messages)
|
||||||
|
print(f"Rephrased query: {rephrased_query}")
|
||||||
|
return rephrased_query if rephrased_query else self.original_question
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error rephrasing query: {e}")
|
||||||
|
return self.original_question
|
||||||
|
|
||||||
def _get_data(self):
|
def _get_data(self):
|
||||||
if self.chunks == 0:
|
if self.chunks == 0:
|
||||||
@@ -69,68 +95,20 @@ class ClassicRAG(BaseRetriever):
|
|||||||
|
|
||||||
return docs
|
return docs
|
||||||
|
|
||||||
def gen(self):
|
def gen():
|
||||||
docs = self._get_data()
|
pass
|
||||||
|
|
||||||
# join all page_content together with a newline
|
def search(self, query: str = ""):
|
||||||
docs_together = "\n".join([doc["text"] for doc in docs])
|
if query:
|
||||||
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
|
self.original_question = query
|
||||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
self.question = self._rephrase_query()
|
||||||
for doc in docs:
|
|
||||||
yield {"source": doc}
|
|
||||||
|
|
||||||
if len(self.chat_history) > 0:
|
|
||||||
for i in self.chat_history:
|
|
||||||
if "prompt" in i and "response" in i:
|
|
||||||
messages_combine.append({"role": "user", "content": i["prompt"]})
|
|
||||||
messages_combine.append(
|
|
||||||
{"role": "assistant", "content": i["response"]}
|
|
||||||
)
|
|
||||||
if "tool_calls" in i:
|
|
||||||
for tool_call in i["tool_calls"]:
|
|
||||||
call_id = tool_call.get("call_id")
|
|
||||||
if call_id is None or call_id == "None":
|
|
||||||
call_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
function_call_dict = {
|
|
||||||
"function_call": {
|
|
||||||
"name": tool_call.get("action_name"),
|
|
||||||
"args": tool_call.get("arguments"),
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.get("action_name"),
|
|
||||||
"response": {"result": tool_call.get("result")},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages_combine.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
|
||||||
messages_combine.append(
|
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
|
||||||
)
|
|
||||||
|
|
||||||
messages_combine.append({"role": "user", "content": self.question})
|
|
||||||
completion = self.agent.gen(messages_combine)
|
|
||||||
|
|
||||||
for line in completion:
|
|
||||||
yield {"answer": str(line)}
|
|
||||||
|
|
||||||
yield {"tool_calls": self.agent.tool_calls.copy()}
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
return self._get_data()
|
return self._get_data()
|
||||||
|
|
||||||
def get_params(self):
|
def get_params(self):
|
||||||
return {
|
return {
|
||||||
"question": self.question,
|
"question": self.original_question,
|
||||||
|
"rephrased_question": self.question,
|
||||||
"source": self.vectorstore,
|
"source": self.vectorstore,
|
||||||
"chat_history": self.chat_history,
|
|
||||||
"prompt": self.prompt,
|
|
||||||
"chunks": self.chunks,
|
"chunks": self.chunks,
|
||||||
"token_limit": self.token_limit,
|
"token_limit": self.token_limit,
|
||||||
"gpt_model": self.gpt_model,
|
"gpt_model": self.gpt_model,
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import json
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class LLMHandler(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def handle_response(self, agent, resp, tools_dict, messages, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAILLMHandler(LLMHandler):
|
|
||||||
def handle_response(self, agent, resp, tools_dict, messages):
|
|
||||||
while resp.finish_reason == "tool_calls":
|
|
||||||
message = json.loads(resp.model_dump_json())["message"]
|
|
||||||
keys_to_remove = {"audio", "function_call", "refusal"}
|
|
||||||
filtered_data = {
|
|
||||||
k: v for k, v in message.items() if k not in keys_to_remove
|
|
||||||
}
|
|
||||||
messages.append(filtered_data)
|
|
||||||
|
|
||||||
tool_calls = resp.message.tool_calls
|
|
||||||
for call in tool_calls:
|
|
||||||
try:
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, call
|
|
||||||
)
|
|
||||||
function_call_dict = {
|
|
||||||
"function_call": {
|
|
||||||
"name": call.function.name,
|
|
||||||
"args": call.function.arguments,
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": call.function.name,
|
|
||||||
"response": {"result": tool_response},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": f"Error executing tool: {str(e)}",
|
|
||||||
"tool_call_id": call_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
resp = agent.llm.gen(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleLLMHandler(LLMHandler):
|
|
||||||
def handle_response(self, agent, resp, tools_dict, messages):
|
|
||||||
from google.genai import types
|
|
||||||
|
|
||||||
while True:
|
|
||||||
response = agent.llm.gen(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
if response.candidates and response.candidates[0].content.parts:
|
|
||||||
tool_call_found = False
|
|
||||||
for part in response.candidates[0].content.parts:
|
|
||||||
if part.function_call:
|
|
||||||
tool_call_found = True
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, part.function_call
|
|
||||||
)
|
|
||||||
function_response_part = types.Part.from_function_response(
|
|
||||||
name=part.function_call.name,
|
|
||||||
response={"result": tool_response},
|
|
||||||
)
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "model", "content": [part.to_json_dict()]}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": [function_response_part.to_json_dict()],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not tool_call_found
|
|
||||||
and response.candidates[0].content.parts
|
|
||||||
and response.candidates[0].content.parts[0].text
|
|
||||||
):
|
|
||||||
return response.candidates[0].content.parts[0].text
|
|
||||||
elif not tool_call_found:
|
|
||||||
return response.candidates[0].content.parts
|
|
||||||
|
|
||||||
else:
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get_llm_handler(llm_type):
|
|
||||||
handlers = {
|
|
||||||
"openai": OpenAILLMHandler(),
|
|
||||||
"google": GoogleLLMHandler(),
|
|
||||||
}
|
|
||||||
return handlers.get(llm_type, OpenAILLMHandler())
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class ToolActionParser:
|
|
||||||
def __init__(self, llm_type):
|
|
||||||
self.llm_type = llm_type
|
|
||||||
self.parsers = {
|
|
||||||
"OpenAILLM": self._parse_openai_llm,
|
|
||||||
"GoogleLLM": self._parse_google_llm,
|
|
||||||
}
|
|
||||||
|
|
||||||
def parse_args(self, call):
|
|
||||||
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
|
|
||||||
return parser(call)
|
|
||||||
|
|
||||||
def _parse_openai_llm(self, call):
|
|
||||||
call_args = json.loads(call.function.arguments)
|
|
||||||
tool_id = call.function.name.split("_")[-1]
|
|
||||||
action_name = call.function.name.rsplit("_", 1)[0]
|
|
||||||
return tool_id, action_name, call_args
|
|
||||||
|
|
||||||
def _parse_google_llm(self, call):
|
|
||||||
call_args = call.args
|
|
||||||
tool_id = call.name.split("_")[-1]
|
|
||||||
action_name = call.name.rsplit("_", 1)[0]
|
|
||||||
return tool_id, action_name, call_args
|
|
||||||
+11
-5
@@ -1,7 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.utils import num_tokens_from_string, num_tokens_from_object_or_list
|
from application.utils import num_tokens_from_object_or_list, num_tokens_from_string
|
||||||
|
|
||||||
mongo = MongoDB.get_client()
|
mongo = MongoDB.get_client()
|
||||||
db = mongo["docsgpt"]
|
db = mongo["docsgpt"]
|
||||||
@@ -24,13 +25,16 @@ def gen_token_usage(func):
|
|||||||
def wrapper(self, model, messages, stream, tools, **kwargs):
|
def wrapper(self, model, messages, stream, tools, **kwargs):
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message["content"]:
|
if message["content"]:
|
||||||
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
|
self.token_usage["prompt_tokens"] += num_tokens_from_string(
|
||||||
|
message["content"]
|
||||||
|
)
|
||||||
result = func(self, model, messages, stream, tools, **kwargs)
|
result = func(self, model, messages, stream, tools, **kwargs)
|
||||||
# check if result is a string
|
|
||||||
if isinstance(result, str):
|
if isinstance(result, str):
|
||||||
self.token_usage["generated_tokens"] += num_tokens_from_string(result)
|
self.token_usage["generated_tokens"] += num_tokens_from_string(result)
|
||||||
else:
|
else:
|
||||||
self.token_usage["generated_tokens"] += num_tokens_from_object_or_list(result)
|
self.token_usage["generated_tokens"] += num_tokens_from_object_or_list(
|
||||||
|
result
|
||||||
|
)
|
||||||
update_token_usage(self.user_api_key, self.token_usage)
|
update_token_usage(self.user_api_key, self.token_usage)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -40,7 +44,9 @@ def gen_token_usage(func):
|
|||||||
def stream_token_usage(func):
|
def stream_token_usage(func):
|
||||||
def wrapper(self, model, messages, stream, tools, **kwargs):
|
def wrapper(self, model, messages, stream, tools, **kwargs):
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
|
self.token_usage["prompt_tokens"] += num_tokens_from_string(
|
||||||
|
message["content"]
|
||||||
|
)
|
||||||
batch = []
|
batch = []
|
||||||
result = func(self, model, messages, stream, tools, **kwargs)
|
result = func(self, model, messages, stream, tools, **kwargs)
|
||||||
for r in result:
|
for r in result:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 194.18 227.53"><defs><style>.cls-1{fill-rule:evenodd;fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" y1="116.23" x2="194.18" y2="116.23" gradientTransform="matrix(1, 0, 0, -1, 0, 230)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff5601"/><stop offset="0.5" stop-color="#ff4000"/><stop offset="1" stop-color="#ff1f01"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M187.39,54.58l5.34-13.1s-6.8-7.27-15-15.52S152,22.56,152,22.56L132,0H62.14L42.23,22.56S24.76,17.71,16.51,26s-15,15.52-15,15.52L6.8,54.58,0,74s20,75.65,22.33,84.89c4.61,18.19,7.77,25.22,20.88,34.44S80.1,218.55,84,221s8.74,6.56,13.11,6.56,9.22-4.13,13.11-6.56,27.67-18.43,40.78-27.65,16.26-16.25,20.87-34.44C174.19,149.64,194.18,74,194.18,74Z"/><path class="cls-2" d="M121.85,41c2.91,0,24.51-4.12,24.51-4.12S172,67.8,172,74.41c0,5.47-2.21,7.6-4.8,10.12-.54.53-1.1,1.08-1.66,1.67l-19.2,20.37-.63.64c-1.91,1.92-4.73,4.76-2.74,9.47l.41,1c2.18,5.1,4.87,11.39,1.44,17.78-3.64,6.78-9.89,11.31-13.9,10.56s-13.41-5.66-16.87-7.9S99.6,126.8,99.6,123.35c0-2.89,7.88-7.68,11.71-10,.77-.47,1.37-.83,1.71-1.07l1.88-1.18c3.49-2.17,9.8-6.09,10-7.83.2-2.14.12-2.77-2.69-8.06-.6-1.13-1.3-2.33-2-3.58-2.68-4.61-5.69-9.78-5-13.48.75-4.18,7.3-6.57,12.85-8.6l2-.75,5.78-2.17c5.54-2.07,11.69-4.37,12.71-4.84,1.4-.65,1-1.27-3.22-1.67l-2.06-.21c-5.27-.56-15-1.59-19.71-.28l-3.06.84c-5.31,1.43-11.81,3.19-12.44,4.21-.11.18-.22.33-.32.47-.6.85-1,1.41-.32,5,.19,1.08.6,3.19,1.1,5.81,1.46,7.65,3.75,19.58,4,22.26,0,.38.08.74.13,1.09.36,3,.61,5-2.87,5.77l-.91.21c-3.92.9-9.67,2.22-11.75,2.22s-7.83-1.32-11.76-2.22l-.9-.21c-3.48-.79-3.23-2.78-2.87-5.77,0-.35.09-.71.13-1.09.29-2.68,2.58-14.65,4-22.3.5-2.59.9-4.7,1.1-5.77.66-3.6.27-4.16-.33-5-.1-.14-.21-.29-.32-.47-.62-1-7.13-2.78-12.43-4.21l-3.07-.84C66,58.31,56.25,59.34,51,59.9l-2.06.21c-4.26.4-4.62,1-3.22,1.67,1,.47,7.17,2.77,12.71,4.84l5.78,2.17,2,.75c5.55,2,12.1,4.42,12.85,8.6.67,3.7-2.34,8.87-5,13.48-.72,1.25-1.43,2.45-2,3.58-2.82,5.29-2.9,5.92-2.7,8.06.16,1.74,6.47,5.66,10,7.83.82.5,1.48.92,1.88,1.18s.94.6,1.71,1.06c3.83,2.33,11.71,7.13,11.71,10,0,3.45-11,12.49-14.42,14.73S67.3,145.24,63.29,146,53,142.2,49.39,135.42c-3.43-6.38-.74-12.68,1.44-17.78l.41-1c2-4.71-.83-7.55-2.74-9.47l-.63-.64L28.67,86.2c-.56-.59-1.12-1.14-1.66-1.67-2.59-2.52-4.79-4.65-4.79-10.12,0-6.61,25.6-37.53,25.6-37.53S69.42,41,72.33,41c2.33,0,6.82-1.55,11.49-3.16l3.56-1.21a34.33,34.33,0,0,1,9.71-2,34.33,34.33,0,0,1,9.71,2c1.18.39,2.37.81,3.56,1.21C115,39.45,119.52,41,121.85,41Z"/><path class="cls-2" d="M118.14,150.39c4.57,2.35,7.81,4,9,4.78,1.59,1,.62,2.86-.82,3.88s-20.85,16-22.73,17.69l-.76.68c-1.82,1.64-4.13,3.72-5.77,3.72s-4-2.08-5.77-3.72l-.76-.68c-1.88-1.66-21.28-16.67-22.73-17.69s-2.41-2.89-.82-3.88c1.23-.77,4.47-2.44,9-4.79l4.34-2.24c6.84-3.54,15.37-6.54,16.7-6.54s9.86,3,16.7,6.54Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -140,7 +140,15 @@ const ConversationBubble = forwardRef<
|
|||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={t('conversation.edit.placeholder')}
|
placeholder={t('conversation.edit.placeholder')}
|
||||||
onChange={(e) => setEditInputBox(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setEditInputBox(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if(e.key === 'Enter' && !e.shiftKey){
|
||||||
|
e.preventDefault();
|
||||||
|
handleEditClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
rows={5}
|
rows={5}
|
||||||
value={editInputBox}
|
value={editInputBox}
|
||||||
className="w-full resize-none border border-silver dark:border-philippine-grey rounded-3xl px-4 py-3 text-base leading-relaxed text-carbon dark:text-chinese-white dark:bg-raisin-black focus:outline-none"
|
className="w-full resize-none border border-silver dark:border-philippine-grey rounded-3xl px-4 py-3 text-base leading-relaxed text-carbon dark:text-chinese-white dark:bg-raisin-black focus:outline-none"
|
||||||
|
|||||||
Reference in new issue
Block a user