sunholo 0.68.0__py3-none-any.whl → 0.69.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,7 +103,7 @@ def create_message_element(message: dict):
103
103
  if 'text' in message: # This is a Slack or Google Chat message
104
104
  log.info(f"Found text element - {message['text']}")
105
105
  return message['text']
106
- elif 'content' in message: # Discord message
106
+ elif 'content' in message: # Discord or OpenAI history message
107
107
  log.info(f"Found content element - {message['content']}")
108
108
  return message['content']
109
109
  else:
@@ -130,6 +130,8 @@ def is_human(message: dict):
130
130
  return message["name"] == "Human"
131
131
  elif 'sender' in message: # Google Chat
132
132
  return message['sender']['type'] == 'HUMAN'
133
+ elif 'role' in message:
134
+ return message['role'] == 'user'
133
135
  else:
134
136
  # Slack: Check for the 'user' field and absence of 'bot_id' field
135
137
  return 'user' in message and 'bot_id' not in message
@@ -174,5 +176,7 @@ def is_ai(message: dict):
174
176
  return message["name"] == "AI"
175
177
  elif 'sender' in message: # Google Chat
176
178
  return message['sender']['type'] == 'BOT'
179
+ elif 'role' in message:
180
+ return message['role'] == 'assistant'
177
181
  else:
178
182
  return 'bot_id' in message # Slack
@@ -0,0 +1,501 @@
1
+ import json
2
+ import traceback
3
+ import datetime
4
+ import uuid
5
+
6
+ from ...agents import extract_chat_history, handle_special_commands
7
+ from ...qna.parsers import parse_output
8
+ from ...streaming import start_streaming_chat
9
+ from ...archive import archive_qa
10
+ from ...logging import log
11
+ from ...utils.config import load_config
12
+ from ...utils.version import sunholo_version
13
+ import os
14
+ from ...gcs.add_file import add_file_to_gcs, handle_base64_image
15
+ from ..swagger import validate_api_key
16
+ from datetime import datetime, timedelta
17
+
18
+ try:
19
+ from flask import request, jsonify, Response
20
+ except ImportError:
21
+ pass
22
+
23
+ try:
24
+ from langfuse.decorators import langfuse_context, observe
25
+ except ImportError:
26
+ pass
27
+
28
+ # Cache dictionary to store validated API keys
29
+ api_key_cache = {}
30
+ cache_duration = timedelta(minutes=5) # Cache duration
31
+
32
+ class VACRoutes:
33
+ """
34
+ **Usage Example:**
35
+
36
+ ```python
37
+ from agents.flask import VACRoutes
38
+
39
+ app = Flask(__name__)
40
+
41
+ def stream_interpreter(question, vector_name, chat_history, **kwargs):
42
+ # Implement your streaming logic
43
+ ...
44
+
45
+ def vac_interpreter(question, vector_name, chat_history, **kwargs):
46
+ # Implement your static VAC logic
47
+ ...
48
+
49
+ vac_routes = VACRoutes(app, stream_interpreter, vac_interpreter)
50
+
51
+ if __name__ == "__main__":
52
+ app.run(debug=True)
53
+ ```
54
+
55
+ """
56
+ def __init__(self, app, stream_interpreter, vac_interpreter):
57
+ self.app = app
58
+ self.stream_interpreter = stream_interpreter
59
+ self.vac_interpreter = vac_interpreter
60
+ self.register_routes()
61
+
62
+ def register_routes(self):
63
+ """
64
+ Registers all the VAC routes for the Flask application.
65
+ """
66
+ # Basic routes
67
+ self.app.route("/", methods=['GET'])(self.home)
68
+ self.app.route("/health", methods=['GET'])(self.health)
69
+
70
+ # Streaming VAC
71
+ self.app.route('/vac/streaming/<vector_name>', methods=['POST'])(self.handle_stream_vac)
72
+
73
+ # Static VAC
74
+ self.app.route('/vac/<vector_name>', methods=['POST'])(self.handle_process_vac)
75
+
76
+ # Authentication middleware
77
+ self.app.before_request(self.check_authentication)
78
+
79
+ # OpenAI health endpoint
80
+ self.app.route('/openai/health', methods=['GET', 'POST'])(self.openai_health_endpoint)
81
+
82
+ # OpenAI compatible endpoint
83
+ self.app.route('/openai/v1/chat/completions', methods=['POST'])(self.handle_openai_compatible_endpoint)
84
+ self.app.route('/openai/v1/chat/completions/<vector_name>', methods=['POST'])(self.handle_openai_compatible_endpoint)
85
+
86
+ def home(self):
87
+ return jsonify("OK")
88
+
89
+ def health(self):
90
+ return jsonify({"status": "healthy"})
91
+
92
+ def make_openai_response(self, user_message, vector_name, answer):
93
+ response_id = str(uuid.uuid4())
94
+ log.info("openai response: Q: {user_message} to VECTOR_NAME: {vector_name} - A: {answer}")
95
+ openai_response = {
96
+ "id": response_id,
97
+ "object": "chat.completion",
98
+ "created": str(int(datetime.now().timestamp())),
99
+ "model": vector_name,
100
+ "system_fingerprint": sunholo_version(),
101
+ "choices": [{
102
+ "index": 0,
103
+ "message": {
104
+ "role": "assistant",
105
+ "content": answer,
106
+ },
107
+ "logprobs": None,
108
+ "finish_reason": "stop"
109
+ }],
110
+ "usage": {
111
+ "prompt_tokens": len(user_message.split()),
112
+ "completion_tokens": len(answer.split()),
113
+ "total_tokens": len(user_message.split()) + len(answer.split())
114
+ }
115
+ }
116
+
117
+ log.info(f"OpenAI response: {openai_response}")
118
+ return jsonify(openai_response)
119
+
120
+
121
+ def handle_stream_vac(self, vector_name):
122
+ observed_stream_interpreter = observe()(self.stream_interpreter)
123
+ prep = self.prep_vac(request, vector_name)
124
+ log.debug(f"Processing prep: {prep}")
125
+ trace = prep["trace"]
126
+ span = prep["span"]
127
+ command_response = prep["command_response"]
128
+ vac_config = prep["vac_config"]
129
+ all_input = prep["all_input"]
130
+
131
+ if command_response:
132
+ return jsonify(command_response)
133
+
134
+ log.info(f'Streaming data with: {all_input}')
135
+ if span:
136
+ generation = span.generation(
137
+ name="start_streaming_chat",
138
+ metadata=vac_config,
139
+ input = all_input,
140
+ completion_start_time=datetime.datetime.now(),
141
+ model=vac_config.get("model") or vac_config.get("llm")
142
+ )
143
+
144
+ def generate_response_content():
145
+
146
+ for chunk in start_streaming_chat(question=all_input["user_input"],
147
+ vector_name=vector_name,
148
+ qna_func=observed_stream_interpreter,
149
+ chat_history=all_input["chat_history"],
150
+ wait_time=all_input["stream_wait_time"],
151
+ timeout=all_input["stream_timeout"],
152
+ #kwargs
153
+ **all_input["kwargs"]
154
+ ):
155
+ if isinstance(chunk, dict) and 'answer' in chunk:
156
+ # When we encounter the dictionary, we yield it as a JSON string
157
+ # and stop the generator.
158
+ if trace:
159
+ chunk["trace"] = trace.id
160
+ chunk["trace_url"] = trace.get_trace_url()
161
+ archive_qa(chunk, vector_name)
162
+ if trace:
163
+ generation.end(output=json.dumps(chunk))
164
+ span.end(output=json.dumps(chunk))
165
+ trace.update(output=json.dumps(chunk))
166
+
167
+ return json.dumps(chunk)
168
+
169
+ else:
170
+ # Otherwise, we yield the plain text chunks as they come in.
171
+ yield chunk
172
+
173
+ # Here, the generator function will handle streaming the content to the client.
174
+ response = Response(generate_response_content(), content_type='text/plain; charset=utf-8')
175
+ response.headers['Transfer-Encoding'] = 'chunked'
176
+
177
+ log.debug(f"streaming response: {response}")
178
+ if trace:
179
+ generation.end(output=response)
180
+ span.end(output=response)
181
+ trace.update(output=response)
182
+
183
+ return response
184
+
185
+ def handle_process_vac(self, vector_name):
186
+ observed_vac_interpreter = observe()(self.vac_interpreter)
187
+ prep = self.prep_vac(request, vector_name)
188
+ log.debug(f"Processing prep: {prep}")
189
+ trace = prep["trace"]
190
+ span = prep["span"]
191
+ command_response = prep["command_response"]
192
+ vac_config = prep["vac_config"]
193
+ all_input = prep["all_input"]
194
+
195
+ if command_response:
196
+ return jsonify(command_response)
197
+
198
+ try:
199
+ if span:
200
+ generation = span.generation(
201
+ name="vac_interpreter",
202
+ metadata=vac_config,
203
+ input = all_input,
204
+ model=vac_config.get("model") or vac_config.get("llm")
205
+ )
206
+ bot_output = observed_vac_interpreter(
207
+ question=all_input["user_input"],
208
+ vector_name=vector_name,
209
+ chat_history=all_input["chat_history"],
210
+ **all_input["kwargs"]
211
+ )
212
+ if span:
213
+ generation.end(output=bot_output)
214
+ # {"answer": "The answer", "source_documents": [{"page_content": "The page content", "metadata": "The metadata"}]}
215
+ bot_output = parse_output(bot_output)
216
+ if trace:
217
+ bot_output["trace"] = trace.id
218
+ bot_output["trace_url"] = trace.get_trace_url()
219
+ archive_qa(bot_output, vector_name)
220
+ log.info(f'==LLM Q:{all_input["user_input"]} - A:{bot_output}')
221
+
222
+
223
+ except Exception as err:
224
+ bot_output = {'answer': f'QNA_ERROR: An error occurred while processing /vac/{vector_name}: {str(err)} traceback: {traceback.format_exc()}'}
225
+
226
+ if trace:
227
+ span.end(output=jsonify(bot_output))
228
+ trace.update(output=jsonify(bot_output))
229
+
230
+ # {'answer': 'output'}
231
+ return jsonify(bot_output)
232
+
233
+ def check_authentication(self):
234
+ if request.path.startswith('/openai/'):
235
+ log.debug(f'Request headers: {request.headers}')
236
+ # the header forwarded
237
+ auth_header = request.headers.get('X-Forwarded-Authorization')
238
+ if auth_header:
239
+
240
+ if auth_header.startswith('Bearer '):
241
+ api_key = auth_header.split(' ')[1] # Assuming "Bearer <api_key>"
242
+ else:
243
+ return jsonify({'error': 'Invalid authorization header does not start with "Bearer " - got: {auth_header}'}), 401
244
+
245
+ endpoints_host = os.getenv('_ENDPOINTS_HOST')
246
+ if not endpoints_host:
247
+ return jsonify({'error': '_ENDPOINTS_HOST environment variable not found'}), 401
248
+
249
+ # Check cache first
250
+ current_time = datetime.now()
251
+ if api_key in api_key_cache:
252
+ cached_result, cache_time = api_key_cache[api_key]
253
+ if current_time - cache_time < cache_duration:
254
+ if not cached_result:
255
+ return jsonify({'error': 'Invalid cached API key'}), 401
256
+ else:
257
+ return # Valid API key, continue to the endpoint
258
+ else:
259
+ # Cache expired, remove from cache
260
+ del api_key_cache[api_key]
261
+
262
+ # Validate API key
263
+ is_valid = validate_api_key(api_key, endpoints_host)
264
+ # Update cache
265
+ api_key_cache[api_key] = (is_valid, current_time)
266
+
267
+ if not is_valid:
268
+ return jsonify({'error': 'Invalid API key'}), 401
269
+ else:
270
+ return jsonify({'error': 'Missing Authorization header'}), 401
271
+
272
+ def openai_health_endpoint():
273
+ return jsonify({'message': 'Success'})
274
+
275
+ def handle_openai_compatible_endpoint(self, vector_name=None):
276
+ data = request.get_json()
277
+ log.info(f'openai_compatible_endpoint got data: {data} for vector: {vector_name}')
278
+
279
+ vector_name = vector_name or data.pop('model', None)
280
+ messages = data.pop('messages', None)
281
+ chat_history = data.pop('chat_history', None)
282
+ stream = data.pop('stream', False)
283
+
284
+ if not messages:
285
+ return jsonify({"error": "No messages provided"}), 400
286
+
287
+ user_message = None
288
+ image_uri = None
289
+ mime_type = None
290
+
291
+ for msg in reversed(messages):
292
+ if msg['role'] == 'user':
293
+ if isinstance(msg['content'], list):
294
+ for content_item in msg['content']:
295
+ if content_item['type'] == 'text':
296
+ user_message = content_item['text']
297
+ elif content_item['type'] == 'image_url':
298
+ base64_data = content_item['image_url']['url']
299
+ image_uri, mime_type = handle_base64_image(base64_data, vector_name)
300
+ else:
301
+ user_message = msg['content']
302
+ break
303
+
304
+ if not user_message:
305
+ return jsonify({"error": "No user message provided"}), 400
306
+ else:
307
+ log.info(f"User message: {user_message}")
308
+
309
+ paired_messages = extract_chat_history(chat_history)
310
+ command_response = handle_special_commands(user_message, vector_name, paired_messages)
311
+
312
+ if command_response is not None:
313
+
314
+ return self.make_openai_response(user_message, vector_name, command_response)
315
+
316
+ if image_uri:
317
+ data["image_uri"] = image_uri
318
+ data["mime"] = mime_type
319
+
320
+ all_input = {
321
+ "user_input": user_message,
322
+ "chat_history": chat_history,
323
+ "kwargs": data
324
+ }
325
+
326
+ observed_stream_interpreter = observe()(self.stream_interpreter)
327
+
328
+ response_id = str(uuid.uuid4())
329
+
330
+ def generate_response_content():
331
+ for chunk in start_streaming_chat(question=user_message,
332
+ vector_name=vector_name,
333
+ qna_func=observed_stream_interpreter,
334
+ chat_history=all_input["chat_history"],
335
+ wait_time=all_input.get("stream_wait_time", 1),
336
+ timeout=all_input.get("stream_timeout", 60),
337
+ **all_input["kwargs"]
338
+ ):
339
+ if isinstance(chunk, dict) and 'answer' in chunk:
340
+ openai_chunk = {
341
+ "id": response_id,
342
+ "object": "chat.completion.chunk",
343
+ "created": str(int(datetime.now().timestamp())),
344
+ "model": vector_name,
345
+ "system_fingerprint": sunholo_version(),
346
+ "choices": [{
347
+ "index": 0,
348
+ "delta": {"content": chunk['answer']},
349
+ "logprobs": None,
350
+ "finish_reason": None
351
+ }]
352
+ }
353
+ yield json.dumps(openai_chunk) + "\n"
354
+ else:
355
+ log.info(f"Unknown chunk: {chunk}")
356
+
357
+ final_chunk = {
358
+ "id": response_id,
359
+ "object": "chat.completion.chunk",
360
+ "created": str(int(datetime.now().timestamp())),
361
+ "model": vector_name,
362
+ "system_fingerprint": sunholo_version(),
363
+ "choices": [{
364
+ "index": 0,
365
+ "delta": {},
366
+ "logprobs": None,
367
+ "finish_reason": "stop"
368
+ }]
369
+ }
370
+ yield json.dumps(final_chunk) + "\n"
371
+
372
+ if stream:
373
+ log.info("Streaming openai chunks")
374
+ return Response(generate_response_content(), content_type='text/plain; charset=utf-8')
375
+
376
+ try:
377
+ observed_vac_interpreter = observe()(self.vac_interpreter)
378
+ bot_output = observed_vac_interpreter(
379
+ question=user_message,
380
+ vector_name=vector_name,
381
+ chat_history=all_input["chat_history"],
382
+ **all_input["kwargs"]
383
+ )
384
+ bot_output = parse_output(bot_output)
385
+
386
+ log.info(f"Bot output: {bot_output}")
387
+ if bot_output:
388
+ return self.make_openai_response(user_message, vector_name, bot_output.get('answer', ''))
389
+ else:
390
+ return self.make_openai_response(user_message, vector_name, 'ERROR: could not find an answer')
391
+
392
+ except Exception as err:
393
+ log.error(f"OpenAI response error: {str(err)} traceback: {traceback.format_exc()}")
394
+
395
+ return self.make_openai_response(user_message, vector_name, f'ERROR: {str(err)}')
396
+
397
+
398
+ def create_langfuse_trace(self, request, vector_name):
399
+ try:
400
+ from langfuse import Langfuse
401
+ langfuse = Langfuse()
402
+ except ImportError as err:
403
+ print(f"No langfuse installed for agents.flask.register_qna_routes, install via `pip install sunholo[http]` - {str(err)}")
404
+
405
+ return None
406
+
407
+ user_id = request.headers.get("X-User-ID")
408
+ session_id = request.headers.get("X-Session-ID")
409
+ message_source = request.headers.get("X-Message-Source")
410
+
411
+ package_version = sunholo_version()
412
+ tags = [package_version]
413
+ if message_source:
414
+ tags.append(message_source)
415
+
416
+ return langfuse.trace(
417
+ name = f"/vac/{vector_name}",
418
+ user_id = user_id,
419
+ session_id = session_id,
420
+ tags = tags,
421
+ release = f"sunholo-v{package_version}"
422
+ )
423
+
424
+ def prep_vac(self, request, vector_name):
425
+ trace = self.create_langfuse_trace(request, vector_name)
426
+ span = None
427
+
428
+ if request.content_type.startswith('application/json'):
429
+ data = request.get_json()
430
+ elif request.content_type.startswith('multipart/form-data'):
431
+ data = request.form.to_dict()
432
+ if 'file' in request.files:
433
+ file = request.files['file']
434
+ if file.filename != '':
435
+ log.info(f"Found file: {file.filename} to upload to GCS")
436
+ try:
437
+ image_uri, mime_type = self.handle_file_upload(file, vector_name)
438
+ data["image_uri"] = image_uri
439
+ data["mime"] = mime_type
440
+ except Exception as e:
441
+ return jsonify({'error': str(e), 'traceback': traceback.format_exc()}), 500
442
+ else:
443
+ return jsonify({"error": "No file selected"}), 400
444
+ else:
445
+ return jsonify({"error": "Unsupported content type"}), 400
446
+
447
+ log.info(f"vac/{vector_name} got data: {data}")
448
+
449
+ config, _ = load_config("config/llm_config.yaml")
450
+ vac_configs = config.get("vac")
451
+ if vac_configs:
452
+ vac_config = vac_configs[vector_name]
453
+
454
+ if trace:
455
+ trace.update(input=data, metadata=vac_config)
456
+
457
+ user_input = data.pop('user_input').strip()
458
+ stream_wait_time = data.pop('stream_wait_time', 7)
459
+ stream_timeout = data.pop('stream_timeout', 120)
460
+ chat_history = data.pop('chat_history', None)
461
+ vector_name = data.pop('vector_name', vector_name)
462
+
463
+ paired_messages = extract_chat_history(chat_history)
464
+
465
+ all_input = {'user_input': user_input,
466
+ 'vector_name': vector_name,
467
+ 'chat_history': paired_messages,
468
+ 'stream_wait_time': stream_wait_time,
469
+ 'stream_timeout': stream_timeout,
470
+ 'kwargs': data}
471
+
472
+ if trace:
473
+ span = trace.span(
474
+ name="VAC",
475
+ metadata=vac_config,
476
+ input = all_input
477
+ )
478
+ command_response = handle_special_commands(user_input, vector_name, paired_messages)
479
+ if command_response is not None:
480
+ if trace:
481
+ trace.update(output=jsonify(command_response))
482
+
483
+ return {
484
+ "trace": trace,
485
+ "span": span,
486
+ "command_response": command_response,
487
+ "all_input": all_input,
488
+ "vac_config": vac_config
489
+ }
490
+
491
+
492
+ def handle_file_upload(self, file, vector_name):
493
+ try:
494
+ file.save(file.filename)
495
+ image_uri = add_file_to_gcs(file.filename, vector_name)
496
+ os.remove(file.filename) # Clean up the saved file
497
+ return image_uri, file.mimetype
498
+ except Exception as e:
499
+ raise Exception(f'File upload failed: {str(e)}')
500
+
501
+
@@ -0,0 +1,221 @@
1
+ try:
2
+ from google.api_core.client_options import ClientOptions
3
+ from google.cloud import discoveryengine_v1alpha as discoveryengine
4
+ except ImportError:
5
+ ClientOptions = None
6
+ discoveryengine = None
7
+
8
+ from ..logging import log
9
+
10
+ class DiscoveryEngineClient:
11
+ """
12
+ Client for interacting with Google Cloud Discovery Engine.
13
+
14
+ Args:
15
+ project_id (str): Your Google Cloud project ID.
16
+ data_store_id (str): The ID of your Discovery Engine data store.
17
+ location (str, optional): The location of the data store (default is 'eu').
18
+
19
+ Example:
20
+ ```python
21
+ client = DiscoveryEngineClient(project_id='your-project-id', data_store_id='your-data-store-id')
22
+
23
+ # Create a collection
24
+ collection_name = client.create_collection("my_new_collection")
25
+
26
+ # Perform a search
27
+ search_response = client.get_chunks("your query", "your_collection_id")
28
+
29
+ ```
30
+
31
+ Parsing:
32
+ ```python
33
+ # Perform a search
34
+ search_response = client.get_chunks("your query", "your_collection_id")
35
+
36
+ # Iterate through the search results
37
+ for result in search_response.results:
38
+ # Get the document (which contains the chunks)
39
+ document = result.document
40
+
41
+ # Iterate through the chunks within the document
42
+ for chunk in document.chunks:
43
+ chunk_text = chunk.snippet # Extract the text content of the chunk
44
+ chunk_document_name = chunk.document_name # Get the name of the document the chunk belongs to
45
+
46
+ # Do something with the chunk_text and chunk_document_name (e.g., print, store, etc.)
47
+ print(f"Chunk Text: {chunk_text}")
48
+ print(f"Document Name: {chunk_document_name}")
49
+ ```
50
+ """
51
+ def __init__(self, data_store_id, project_id, location="eu"):
52
+ if not discoveryengine:
53
+ raise ImportError("Google Cloud Discovery Engine not available, install via `pip install sunholo[gcp]`")
54
+
55
+ self.project_id = project_id
56
+ self.data_store_id = data_store_id
57
+ self.location = location
58
+ client_options = (
59
+ ClientOptions(api_endpoint=f"{location}-discoveryengine.googleapis.com")
60
+ if location != "global"
61
+ else None
62
+ )
63
+ self.client = discoveryengine.DataStoreServiceClient(client_options=client_options)
64
+
65
+
66
+ def create_collection(self, collection_id: str) -> str:
67
+ """
68
+ Creates a new collection within the specified data store.
69
+
70
+ Args:
71
+ collection_id (str): The ID of the collection to create.
72
+
73
+ Returns:
74
+ str: The resource name of the created collection.
75
+
76
+ Example:
77
+ ```python
78
+ collection_name = client.create_collection('my_new_collection')
79
+ `
80
+ """
81
+
82
+ parent = self.client.data_store_path(
83
+ project=self.project_id, location=self.location, data_store=self.data_store_id
84
+ )
85
+
86
+ collection = discoveryengine.Collection(display_name=collection_id)
87
+ request = discoveryengine.CreateCollectionRequest(
88
+ parent=parent, collection_id=collection_id, collection=collection
89
+ )
90
+
91
+ operation = self.client.create_collection(request=request)
92
+ log.info(f"Waiting for operation to complete: {operation.operation.name}")
93
+ response = operation.result()
94
+
95
+ return response.name
96
+
97
+ def create_data_store(
98
+ self, chunk_size: int = 500
99
+ ) -> str:
100
+ """
101
+ Creates a new data store with default configuration.
102
+
103
+ Args:
104
+ chunk_size (int, optional): The size of the chunks to create for documents (default is 500).
105
+
106
+ Returns:
107
+ str: The name of the long-running operation for data store creation.
108
+ """
109
+ parent = self.client.common_location_path(project=self.project_id, location=self.location)
110
+
111
+ # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1alpha.types.DocumentProcessingConfig
112
+ doc_config = discoveryengine.DocumentProcessingConfig(
113
+ chunking_config=discoveryengine.DocumentProcessingConfig.ChunkingConfig(
114
+ layout_based_chunking_config=discoveryengine.DocumentProcessingConfig.ChunkingConfig.LayoutBasedChunkingConfig(
115
+ chunk_size=chunk_size,
116
+ include_ancestor_headings=True
117
+ )
118
+ ),
119
+ default_parsing_config=discoveryengine.DocumentProcessingConfig.ParsingConfig(
120
+ layout_parsing_config=discoveryengine.DocumentProcessingConfig.ParsingConfig.LayoutParsingConfig()
121
+ )
122
+ )
123
+
124
+ # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.services.data_store_service.DataStoreServiceClient
125
+ # https://cloud.google.com/python/docs/reference/discoveryengine/0.11.4/google.cloud.discoveryengine_v1alpha.types.DataStore
126
+ data_store = discoveryengine.DataStore(
127
+ display_name=self.data_store_id,
128
+ # Options: GENERIC, MEDIA, HEALTHCARE_FHIR
129
+ industry_vertical=discoveryengine.IndustryVertical.GENERIC,
130
+ # Options: SOLUTION_TYPE_RECOMMENDATION, SOLUTION_TYPE_SEARCH, SOLUTION_TYPE_CHAT, SOLUTION_TYPE_GENERATIVE_CHAT
131
+ solution_types=[discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH],
132
+ # Options: NO_CONTENT, CONTENT_REQUIRED, PUBLIC_WEBSITE
133
+ content_config=discoveryengine.DataStore.ContentConfig.CONTENT_REQUIRED,
134
+ # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.DocumentProcessingConfig
135
+ document_processing_config=doc_config
136
+ )
137
+
138
+ # https://cloud.google.com/python/docs/reference/discoveryengine/0.11.4/google.cloud.discoveryengine_v1alpha.types.CreateDataStoreRequest
139
+ request = discoveryengine.CreateDataStoreRequest(
140
+ parent=parent,
141
+ data_store_id=self.data_store_id,
142
+ data_store=data_store,
143
+ # Optional: For Advanced Site Search Only
144
+ # create_advanced_site_search=True,
145
+ )
146
+
147
+ # Make the request
148
+ operation = self.client.create_data_store(request=request)
149
+
150
+ log.info(f"Waiting for operation to complete: {operation.operation.name}")
151
+ response = operation.result()
152
+
153
+ # Once the operation is complete,
154
+ # get information from operation metadata
155
+ metadata = discoveryengine.CreateDataStoreMetadata(operation.metadata)
156
+
157
+ # Handle the response
158
+ log.info(f"{response=} {metadata=}")
159
+
160
+ return operation.operation.name
161
+
162
+ def get_chunks(
163
+ self,
164
+ query: str,
165
+ collection_id: str,
166
+ num_previous_chunks: int = 3,
167
+ num_next_chunks: int = 3,
168
+ page_size: int = 10,
169
+ doc_or_chunks: str = "CHUNKS", # or DOCUMENTS
170
+ ):
171
+ """Retrieves chunks or documents based on a query.
172
+
173
+ Args:
174
+ query (str): The search query.
175
+ collection_id (str): The ID of the collection to search.
176
+ num_previous_chunks (int, optional): Number of previous chunks to return for context (default is 3).
177
+ num_next_chunks (int, optional): Number of next chunks to return for context (default is 3).
178
+ page_size (int, optional): The maximum number of results to return per page (default is 10).
179
+
180
+ Returns:
181
+ discoveryengine.SearchResponse: The search response object containing the search results.
182
+
183
+ Example:
184
+ ```python
185
+ search_response = client.get_chunks('your query', 'your_collection_id')
186
+ for result in search_response.results:
187
+ for chunk in result.document.chunks:
188
+ print(f"Chunk: {chunk.snippet}, document name: {chunk.document_name}")
189
+ ```
190
+ """
191
+ serving_config = self.client.get_default_serving_config(
192
+ name=self.client.serving_config_path(
193
+ project=self.project_id,
194
+ location=self.location,
195
+ data_store=self.data_store_id,
196
+ serving_config="default_serving_config")
197
+ ).name
198
+
199
+ filter = f'content_search=true AND collection_id="{collection_id}"'
200
+
201
+ search_request = discoveryengine.SearchRequest(
202
+ serving_config=serving_config,
203
+ query=query,
204
+ page_size=page_size,
205
+ filter=filter,
206
+ content_search_spec=discoveryengine.SearchRequest.ContentSearchSpec(
207
+ #snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(
208
+ # return_snippet=True
209
+ #),
210
+ search_result_mode=doc_or_chunks, # CHUNKS or DOCUMENTS
211
+ chunk_spec=discoveryengine.SearchRequest.ContentSearchSpec.ChunkSpec(
212
+ num_previous_chunks=num_previous_chunks,
213
+ num_next_chunks=num_next_chunks,
214
+ ),
215
+ ),
216
+ )
217
+
218
+ search_response = self.client.search(search_request)
219
+
220
+ return search_response
221
+
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.68.0
3
+ Version: 0.69.0
4
4
  Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
5
5
  Home-page: https://github.com/sunholo-data/sunholo-py
6
- Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.68.0.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.69.0.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -1,7 +1,7 @@
1
1
  sunholo/__init__.py,sha256=0CdpufyRKWyZe7J7UKigL6j_qOorM-p0OjHIAuf9M38,864
2
2
  sunholo/logging.py,sha256=00VGGArfWHbJuHHSJ4kXhHTggWnRfbVYMcZNOYIsqnA,11787
3
3
  sunholo/agents/__init__.py,sha256=Hb4NXy2rN-83Z0-UDRwX-LXv2R29lcbSFPf8G6q4fZg,380
4
- sunholo/agents/chat_history.py,sha256=bkII7PNEbGCaobu2Rnr2rM9dim3BCK0kM-tiWhoI1tw,5219
4
+ sunholo/agents/chat_history.py,sha256=8iX1bgvRW6fdp6r_DQR_caPHYrZ_9QJJgPxCiSDf3q8,5380
5
5
  sunholo/agents/dispatch_to_qa.py,sha256=nFNdxhkr7rVYuUwVoBCBNYBI2Dke6-_z_ZApBEWb_cU,8291
6
6
  sunholo/agents/langserve.py,sha256=FdhQjorAY2bMn2rpuabNT6bU3uqSKWrl8DjpH3L_V7k,4375
7
7
  sunholo/agents/pubsub.py,sha256=5hbbhbBGyVWRpt2sAGC5FEheYH1mCCwVUhZEB1S7vGg,1337
@@ -14,6 +14,7 @@ sunholo/agents/fastapi/qna_routes.py,sha256=DgK4Btu5XriOC1JaRQ4G_nWEjJfnQ0J5pyLa
14
14
  sunholo/agents/flask/__init__.py,sha256=uqfHNw2Ru3EJ4dJEcbp86h_lkquBQPMxZbjhV_xe3rs,72
15
15
  sunholo/agents/flask/base.py,sha256=FgSaCODyoTtlstJtsqlLPScdgRUtv9_plxftdzHdVFo,809
16
16
  sunholo/agents/flask/qna_routes.py,sha256=oDZzI0FllRD5GZI_C8EbKvvBSrgRlvmpwQc7lp54Krs,21926
17
+ sunholo/agents/flask/vac_routes.py,sha256=l2-w7x437F0Uu3QvwNueEYPtnKuIee6bHJ7LUMt_tkY,19520
17
18
  sunholo/archive/__init__.py,sha256=qNHWm5rGPVOlxZBZCpA1wTYPbalizRT7f8X4rs2t290,31
18
19
  sunholo/archive/archive.py,sha256=C-UhG5x-XtZ8VheQp92IYJqgD0V3NFQjniqlit94t18,1197
19
20
  sunholo/auth/__init__.py,sha256=4owDjSaWYkbTlPK47UHTOC0gCWbZsqn4ZIEw5NWZTlg,28
@@ -50,6 +51,7 @@ sunholo/database/__init__.py,sha256=Zz0Shcq-CtStf9rJGIYB_Ybzb8rY_Q9mfSj-nviM490,
50
51
  sunholo/database/alloydb.py,sha256=d9W0pbZB0jTVIGF5OVaQ6kXHo-X3-6e9NpWNmV5e9UY,10464
51
52
  sunholo/database/alloydb_client.py,sha256=AYA0SSaBy-1XEfeZI97sMGehfrwnfbwZ8sE0exzI2E0,7254
52
53
  sunholo/database/database.py,sha256=UDHkceiEvJmS3esQX2LYEjEMrHcogN_JHuJXoVWCH3M,7354
54
+ sunholo/database/discovery_engine.py,sha256=GxAUBqtv3Q4z2fN2wcja5nRrQxFUXZMGPukSTA91yDs,9203
53
55
  sunholo/database/lancedb.py,sha256=2rAbJVusMrm5TPtVTsUtmwn0z1iZ_wvbKhc6eyT6ClE,708
54
56
  sunholo/database/static_dbs.py,sha256=aOyU3AJ-Dzz3qSNjbuN2293cfYw5PhkcQuQxdwPMJ4w,435
55
57
  sunholo/database/uuid.py,sha256=GtUL_uq80u2xkozPF9kwNpvhBf03hbZR3xUhO3NomBM,237
@@ -106,9 +108,9 @@ sunholo/vertex/__init__.py,sha256=JvHcGFuv6R_nAhY2AdoqqhMpJ5ugeWPZ_svGhWrObBk,13
106
108
  sunholo/vertex/init.py,sha256=JDMUaBRdednzbKF-5p33qqLit2LMsvgvWW-NRz0AqO0,1801
107
109
  sunholo/vertex/memory_tools.py,sha256=8F1iTWnqEK9mX4W5RzCVKIjydIcNp6OFxjn_dtQ3GXo,5379
108
110
  sunholo/vertex/safety.py,sha256=3meAX0HyGZYrH7rXPUAHxtI_3w_zoy_RX7Shtkoa660,1275
109
- sunholo-0.68.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
110
- sunholo-0.68.0.dist-info/METADATA,sha256=G2k3HIbR1aD0HK5mrD_Rr8E_jusewG5z8dgW_DCsmiA,6155
111
- sunholo-0.68.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
112
- sunholo-0.68.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
113
- sunholo-0.68.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
114
- sunholo-0.68.0.dist-info/RECORD,,
111
+ sunholo-0.69.0.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
112
+ sunholo-0.69.0.dist-info/METADATA,sha256=7wTBdg2KnW47NJ29PhzFqSXsMPXIc36HKKm8jXnmIIs,6155
113
+ sunholo-0.69.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
114
+ sunholo-0.69.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
115
+ sunholo-0.69.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
116
+ sunholo-0.69.0.dist-info/RECORD,,