ParisNeo commited on
Commit
a17cd44
·
1 Parent(s): 119fb65

Added an OpenWebui tool for LightRag

Browse files
Files changed (1) hide show
  1. extra/OpenWebuiTool/openwebui_tool.py +331 -0
extra/OpenWebuiTool/openwebui_tool.py CHANGED
@@ -28,3 +28,334 @@ __version__ = "1.0.0"
28
  __author__ = "ParisNeo"
29
  __author_email__ = "parisneoai@gmail.com"
30
  __description__ = "Lightrag integration for OpenWebui"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  __author__ = "ParisNeo"
29
  __author_email__ = "parisneoai@gmail.com"
30
  __description__ = "Lightrag integration for OpenWebui"
31
+
32
+
33
+ import os
34
+ import requests
35
+ import json
36
+ import csv
37
+ from io import StringIO
38
+ from pydantic import BaseModel, Field
39
+ from typing import Callable, Any, Literal, Union, List, Tuple
40
+
41
+
42
+ class StatusEventEmitter:
43
+ def __init__(self, event_emitter: Callable[[dict], Any] = None):
44
+ self.event_emitter = event_emitter
45
+
46
+ async def emit(self, description="Unknown State", status="in_progress", done=False):
47
+ if self.event_emitter:
48
+ await self.event_emitter(
49
+ {
50
+ "type": "status",
51
+ "data": {
52
+ "status": status,
53
+ "description": description,
54
+ "done": done,
55
+ },
56
+ }
57
+ )
58
+
59
+
60
+ class MessageEventEmitter:
61
+ def __init__(self, event_emitter: Callable[[dict], Any] = None):
62
+ self.event_emitter = event_emitter
63
+
64
+ async def emit(self, content="Some message"):
65
+ if self.event_emitter:
66
+ await self.event_emitter(
67
+ {
68
+ "type": "message",
69
+ "data": {
70
+ "content": content,
71
+ },
72
+ }
73
+ )
74
+
75
+
76
+ class Tools:
77
+ class Valves(BaseModel):
78
+ LIGHTRAG_SERVER_URL: str = Field(
79
+ default="http://localhost:9621/query",
80
+ description="The base URL for the LightRag server",
81
+ )
82
+ MODE: Literal["naive", "local", "global", "hybrid"] = Field(
83
+ default="hybrid",
84
+ description="The mode to use for the LightRag query. Options: naive, local, global, hybrid",
85
+ )
86
+ ONLY_NEED_CONTEXT: bool = Field(
87
+ default=False,
88
+ description="If True, only the context is needed from the LightRag response",
89
+ )
90
+ DEBUG_MODE: bool = Field(
91
+ default=False,
92
+ description="If True, debugging information will be emitted",
93
+ )
94
+ KEY: str = Field(
95
+ default="",
96
+ description="Optional Bearer Key for authentication",
97
+ )
98
+ MAX_ENTITIES: int = Field(
99
+ default=5,
100
+ description="Maximum number of entities to keep",
101
+ )
102
+ MAX_RELATIONSHIPS: int = Field(
103
+ default=5,
104
+ description="Maximum number of relationships to keep",
105
+ )
106
+ MAX_SOURCES: int = Field(
107
+ default=3,
108
+ description="Maximum number of sources to keep",
109
+ )
110
+
111
+ def __init__(self):
112
+ self.valves = self.Valves()
113
+ self.headers = {
114
+ "Content-Type": "application/json",
115
+ "User-Agent": "LightRag-Tool/1.0",
116
+ }
117
+
118
+ async def query_lightrag(
119
+ self,
120
+ query: str,
121
+ __event_emitter__: Callable[[dict], Any] = None,
122
+ ) -> str:
123
+ """
124
+ Query the LightRag server and retrieve information.
125
+ This function must be called before answering the user question
126
+ :params query: The query string to send to the LightRag server.
127
+ :return: The response from the LightRag server in Markdown format or raw response.
128
+ """
129
+ self.status_emitter = StatusEventEmitter(__event_emitter__)
130
+ self.message_emitter = MessageEventEmitter(__event_emitter__)
131
+
132
+ lightrag_url = self.valves.LIGHTRAG_SERVER_URL
133
+ payload = {
134
+ "query": query,
135
+ "mode": str(self.valves.MODE),
136
+ "stream": False,
137
+ "only_need_context": self.valves.ONLY_NEED_CONTEXT,
138
+ }
139
+ await self.status_emitter.emit("Initializing Lightrag query..")
140
+
141
+ if self.valves.DEBUG_MODE:
142
+ await self.message_emitter.emit(
143
+ "### Debug Mode Active\n\nDebugging information will be displayed.\n"
144
+ )
145
+ await self.message_emitter.emit(
146
+ "#### Payload Sent to LightRag Server\n```json\n"
147
+ + json.dumps(payload, indent=4)
148
+ + "\n```\n"
149
+ )
150
+
151
+ # Add Bearer Key to headers if provided
152
+ if self.valves.KEY:
153
+ self.headers["Authorization"] = f"Bearer {self.valves.KEY}"
154
+
155
+ try:
156
+ await self.status_emitter.emit("Sending request to LightRag server")
157
+
158
+ response = requests.post(
159
+ lightrag_url, json=payload, headers=self.headers, timeout=120
160
+ )
161
+ response.raise_for_status()
162
+ data = response.json()
163
+ await self.status_emitter.emit(
164
+ status="complete",
165
+ description="LightRag query Succeeded",
166
+ done=True,
167
+ )
168
+
169
+ # Return parsed Markdown if ONLY_NEED_CONTEXT is True, otherwise return raw response
170
+ if self.valves.ONLY_NEED_CONTEXT:
171
+ try:
172
+ if self.valves.DEBUG_MODE:
173
+ await self.message_emitter.emit(
174
+ "#### LightRag Server Response\n```json\n"
175
+ + data["response"]
176
+ + "\n```\n"
177
+ )
178
+ except Exception as ex:
179
+ if self.valves.DEBUG_MODE:
180
+ await self.message_emitter.emit(
181
+ "#### Exception\n" + str(ex) + "\n"
182
+ )
183
+ return f"Exception: {ex}"
184
+ return data["response"]
185
+ else:
186
+ if self.valves.DEBUG_MODE:
187
+ await self.message_emitter.emit(
188
+ "#### LightRag Server Response\n```json\n"
189
+ + data["response"]
190
+ + "\n```\n"
191
+ )
192
+ await self.status_emitter.emit("Lightrag query success")
193
+ return data["response"]
194
+
195
+ except requests.exceptions.RequestException as e:
196
+ await self.status_emitter.emit(
197
+ status="error",
198
+ description=f"Error during LightRag query: {str(e)}",
199
+ done=True,
200
+ )
201
+ return json.dumps({"error": str(e)})
202
+
203
+ def extract_code_blocks(
204
+ self, text: str, return_remaining_text: bool = False
205
+ ) -> Union[List[dict], Tuple[List[dict], str]]:
206
+ """
207
+ This function extracts code blocks from a given text and optionally returns the text without code blocks.
208
+
209
+ Parameters:
210
+ text (str): The text from which to extract code blocks. Code blocks are identified by triple backticks (```).
211
+ return_remaining_text (bool): If True, also returns the text with code blocks removed.
212
+
213
+ Returns:
214
+ Union[List[dict], Tuple[List[dict], str]]:
215
+ - If return_remaining_text is False: Returns only the list of code block dictionaries
216
+ - If return_remaining_text is True: Returns a tuple containing:
217
+ * List of code block dictionaries
218
+ * String containing the text with all code blocks removed
219
+
220
+ Each code block dictionary contains:
221
+ - 'index' (int): The index of the code block in the text
222
+ - 'file_name' (str): The name of the file extracted from the preceding line, if available
223
+ - 'content' (str): The content of the code block
224
+ - 'type' (str): The type of the code block
225
+ - 'is_complete' (bool): True if the block has a closing tag, False otherwise
226
+ """
227
+ remaining = text
228
+ bloc_index = 0
229
+ first_index = 0
230
+ indices = []
231
+ text_without_blocks = text
232
+
233
+ # Find all code block delimiters
234
+ while len(remaining) > 0:
235
+ try:
236
+ index = remaining.index("```")
237
+ indices.append(index + first_index)
238
+ remaining = remaining[index + 3 :]
239
+ first_index += index + 3
240
+ bloc_index += 1
241
+ except Exception as ex:
242
+ if bloc_index % 2 == 1:
243
+ index = len(remaining)
244
+ indices.append(index)
245
+ remaining = ""
246
+
247
+ code_blocks = []
248
+ is_start = True
249
+
250
+ # Process code blocks and build text without blocks if requested
251
+ if return_remaining_text:
252
+ text_parts = []
253
+ last_end = 0
254
+
255
+ for index, code_delimiter_position in enumerate(indices):
256
+ if is_start:
257
+ block_infos = {
258
+ "index": len(code_blocks),
259
+ "file_name": "",
260
+ "section": "",
261
+ "content": "",
262
+ "type": "",
263
+ "is_complete": False,
264
+ }
265
+
266
+ # Store text before code block if returning remaining text
267
+ if return_remaining_text:
268
+ text_parts.append(text[last_end:code_delimiter_position].strip())
269
+
270
+ # Check the preceding line for file name
271
+ preceding_text = text[:code_delimiter_position].strip().splitlines()
272
+ if preceding_text:
273
+ last_line = preceding_text[-1].strip()
274
+ if last_line.startswith("<file_name>") and last_line.endswith(
275
+ "</file_name>"
276
+ ):
277
+ file_name = last_line[
278
+ len("<file_name>") : -len("</file_name>")
279
+ ].strip()
280
+ block_infos["file_name"] = file_name
281
+ elif last_line.startswith("## filename:"):
282
+ file_name = last_line[len("## filename:") :].strip()
283
+ block_infos["file_name"] = file_name
284
+ if last_line.startswith("<section>") and last_line.endswith(
285
+ "</section>"
286
+ ):
287
+ section = last_line[
288
+ len("<section>") : -len("</section>")
289
+ ].strip()
290
+ block_infos["section"] = section
291
+
292
+ sub_text = text[code_delimiter_position + 3 :]
293
+ if len(sub_text) > 0:
294
+ try:
295
+ find_space = sub_text.index(" ")
296
+ except:
297
+ find_space = int(1e10)
298
+ try:
299
+ find_return = sub_text.index("\n")
300
+ except:
301
+ find_return = int(1e10)
302
+ next_index = min(find_return, find_space)
303
+ if "{" in sub_text[:next_index]:
304
+ next_index = 0
305
+ start_pos = next_index
306
+
307
+ if code_delimiter_position + 3 < len(text) and text[
308
+ code_delimiter_position + 3
309
+ ] in ["\n", " ", "\t"]:
310
+ block_infos["type"] = "language-specific"
311
+ else:
312
+ block_infos["type"] = sub_text[:next_index]
313
+
314
+ if index + 1 < len(indices):
315
+ next_pos = indices[index + 1] - code_delimiter_position
316
+ if (
317
+ next_pos - 3 < len(sub_text)
318
+ and sub_text[next_pos - 3] == "`"
319
+ ):
320
+ block_infos["content"] = sub_text[
321
+ start_pos : next_pos - 3
322
+ ].strip()
323
+ block_infos["is_complete"] = True
324
+ else:
325
+ block_infos["content"] = sub_text[
326
+ start_pos:next_pos
327
+ ].strip()
328
+ block_infos["is_complete"] = False
329
+
330
+ if return_remaining_text:
331
+ last_end = indices[index + 1] + 3
332
+ else:
333
+ block_infos["content"] = sub_text[start_pos:].strip()
334
+ block_infos["is_complete"] = False
335
+
336
+ if return_remaining_text:
337
+ last_end = len(text)
338
+
339
+ code_blocks.append(block_infos)
340
+ is_start = False
341
+ else:
342
+ is_start = True
343
+
344
+ if return_remaining_text:
345
+ # Add any remaining text after the last code block
346
+ if last_end < len(text):
347
+ text_parts.append(text[last_end:].strip())
348
+ # Join all non-code parts with newlines
349
+ text_without_blocks = "\n".join(filter(None, text_parts))
350
+ return code_blocks, text_without_blocks
351
+
352
+ return code_blocks
353
+
354
+ def clean(self, csv_content: str):
355
+ lines = csv_content.splitlines()
356
+ if lines:
357
+ # Remove spaces around headers and ensure no spaces between commas
358
+ header = ",".join([col.strip() for col in lines[0].split(",")])
359
+ lines[0] = header # Replace the first line with the cleaned header
360
+ csv_content = "\n".join(lines)
361
+ return csv_content