pvanand commited on
Commit
1a6d961
·
verified ·
1 Parent(s): f4f1a92

Upload 11 files

Browse files
aiclient.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # aiclient.py
2
+
3
+ import os
4
+ import time
5
+ import json
6
+ from typing import List, Dict, Optional, Union, AsyncGenerator
7
+ from openai import AsyncOpenAI
8
+ from starlette.responses import StreamingResponse
9
+ from observability import log_execution ,LLMObservabilityManager
10
+ import psycopg2
11
+ import requests
12
+ from functools import lru_cache
13
+ import logging
14
+ import pandas as pd
15
+ logger = logging.getLogger(__name__)
16
+
17
+ @lru_cache(maxsize=1)
18
+ def get_model_info():
19
+ try:
20
+ model_info_dict = requests.get(
21
+ 'https://openrouter.ai/api/v1/models',
22
+ headers={'accept': 'application/json'}
23
+ ).json()["data"]
24
+
25
+ # Save the model info to a JSON file
26
+ with open('model_info.json', 'w') as json_file:
27
+ json.dump(model_info_dict, json_file, indent=4)
28
+
29
+ except Exception as e:
30
+ logger.error(f"Failed to fetch model info: {e}. Loading from file.")
31
+ if os.path.exists('model_info.json'):
32
+ with open('model_info.json', 'r') as json_file:
33
+ model_info_dict = json.load(json_file)
34
+ model_info = pd.DataFrame(model_info_dict)
35
+ return model_info
36
+
37
+ else:
38
+ logger.error("No model info file found")
39
+ return None
40
+
41
+ model_info = pd.DataFrame(model_info_dict)
42
+ return model_info
43
+
44
+ class AIClient:
45
+ def __init__(self):
46
+ self.client = AsyncOpenAI(
47
+ base_url="https://openrouter.ai/api/v1",
48
+ api_key=os.environ['OPENROUTER_API_KEY']
49
+ )
50
+ self.observability_manager = LLMObservabilityManager()
51
+ self.model_info = get_model_info()
52
+
53
+ #@log_execution
54
+ async def generate_response(
55
+ self,
56
+ messages: List[Dict[str, str]],
57
+ model: str = "openai/gpt-4o-mini",
58
+ max_tokens: int = 32000,
59
+ conversation_id: Optional[str] = None,
60
+ user: str = "anonymous"
61
+ ) -> AsyncGenerator[str, None]:
62
+ if not messages:
63
+ return
64
+
65
+ start_time = time.time()
66
+ full_response = ""
67
+ usage = {"completion_tokens": 0, "prompt_tokens": 0, "total_tokens": 0}
68
+ status = "success"
69
+
70
+ try:
71
+ response = await self.client.chat.completions.create(
72
+ model=model,
73
+ messages=messages,
74
+ max_tokens=max_tokens,
75
+ stream=True,
76
+ stream_options={"include_usage": True}
77
+ )
78
+ end_time = time.time()
79
+ latency = end_time - start_time
80
+
81
+ async for chunk in response:
82
+ if chunk.choices[0].delta.content:
83
+ yield chunk.choices[0].delta.content
84
+ full_response += chunk.choices[0].delta.content
85
+
86
+ if chunk.usage:
87
+ model = chunk.model
88
+ usage["completion_tokens"] = chunk.usage.completion_tokens
89
+ usage["prompt_tokens"] = chunk.usage.prompt_tokens
90
+ usage["total_tokens"] = chunk.usage.total_tokens
91
+ print(usage)
92
+ print(model)
93
+
94
+ except Exception as e:
95
+ status = "error"
96
+ full_response = str(e)
97
+ latency = time.time() - start_time
98
+ print(f"Error in generate_response: {e}")
99
+
100
+ finally:
101
+ # Log the observation
102
+ try:
103
+ pricing_data = self.model_info[self.model_info.id == model]["pricing"].values[0]
104
+ cost = float(pricing_data["completion"]) * float(usage["completion_tokens"]) + float(pricing_data["prompt"]) * float(usage["prompt_tokens"])
105
+ self.observability_manager.insert_observation(
106
+ response=full_response,
107
+ model=model,
108
+ completion_tokens=usage["completion_tokens"],
109
+ prompt_tokens=usage["prompt_tokens"],
110
+ total_tokens=usage["total_tokens"],
111
+ cost=cost,
112
+ conversation_id=conversation_id or "default",
113
+ status=status,
114
+ request=json.dumps([msg for msg in messages if msg.get('role') != 'system']),
115
+ latency=latency,
116
+ user=user
117
+ )
118
+ except Exception as obs_error:
119
+ print(f"Error logging observation: {obs_error}")
120
+
121
+
122
+ class DatabaseManager:
123
+ """Manages database operations."""
124
+
125
+ def __init__(self):
126
+ self.db_params = {
127
+ "dbname": "postgres",
128
+ "user": os.environ['SUPABASE_USER'],
129
+ "password": os.environ['SUPABASE_PASSWORD'],
130
+ "host": "aws-0-us-west-1.pooler.supabase.com",
131
+ "port": "5432"
132
+ }
133
+
134
+ @log_execution
135
+ def update_database(self, user_id: str, user_query: str, response: str) -> None:
136
+ with psycopg2.connect(**self.db_params) as conn:
137
+ with conn.cursor() as cur:
138
+ insert_query = """
139
+ INSERT INTO ai_document_generator (user_id, user_query, response)
140
+ VALUES (%s, %s, %s);
141
+ """
142
+ cur.execute(insert_query, (user_id, user_query, response))
combined_digi_yatra.csv ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "title/question","paragraph/answer","url"
2
+ "Introduction to Digi Yatra","Digi Yatra is a decentralized digital wallet system for air travel in India. It aims to provide a seamless and paperless journey for passengers by automating identity verification and boarding pass checks at airports. The system does not store any centralized data, addressing privacy concerns.",""
3
+ "How Digi Yatra Works","Users register on the Digi Yatra app by linking their Aadhaar (national ID) and creating a verified credential. When traveling, they link their boarding pass to the app. At the airport, facial recognition is used to verify identity and boarding pass details at entry, security, and boarding gates.",""
4
+ "Benefits and Adoption","Digi Yatra has been adopted by 4.5 million users and used 19 million times across 14 airports in 15 months since launch. It improves security by validating IDs against original databases and automating manual checks. The system is especially appreciated by older travelers for its ease of use.",""
5
+ "Privacy and Data Handling","No passenger data is stored centrally by Digi Yatra. Airports only keep biometric data for 24 hours as per regulations. Users can delete their data from the app at any time. The system aims for 'privacy by design' while facilitating secure data sharing between stakeholders.",""
6
+ "Challenges and Future Improvements","Current challenges include standardizing data formats across airlines and airports, integrating with more identity providers beyond Aadhaar, and expanding to international travel. Future improvements may include direct integration with airline reservation systems and adding support for passport-based verification.",""
7
+ "Stakeholder Integration","Digi Yatra aims to improve data sharing between airports, airlines, and security agencies to create a truly seamless passenger journey. However, this requires overcoming technical and regulatory hurdles in standardizing data exchange and access.",""
8
+ "What is Digi Yatra?","Digi Yatra is a digital platform that aims to make air travel in India seamless and hassle-free. It uses facial recognition technology to allow passengers to go through various checkpoints at airports without needing to show physical documents. The service is voluntary and passengers can choose to use manual processes if they prefer.",""
9
+ "How does Digi Yatra work?","Digi Yatra works by allowing passengers to create a digital identity using their Aadhaar card and a selfie. This identity is then linked to their boarding pass. At the airport, facial recognition cameras at various checkpoints verify the passenger's identity, allowing them to proceed without showing physical documents.",""
10
+ "Is Digi Yatra mandatory?","No, Digi Yatra is not mandatory. It is a voluntary service that passengers can choose to use. Those who prefer can still opt for manual document checks at airports.",""
11
+ "What are the benefits of using Digi Yatra?","The main benefits of using Digi Yatra include faster airport processing, reduced queuing times, seamless and contactless passage through various checkpoints, and a more hygienic travel experience as it minimizes physical contact and document exchanges.",""
12
+ "How do I enroll for Digi Yatra?","You can enroll for Digi Yatra by downloading the Digi Yatra app and creating your digital identity using your Aadhaar card and a selfie. The app is available for both iOS and Android devices.",""
13
+ "Is my data safe with Digi Yatra?","Digi Yatra is built on the principles of privacy by design. Your personally identifiable information (PII) is not stored in any central repository. It is only stored on your smartphone in the Digi Yatra app's secure wallet. Data shared with airports is purged within 24 hours of your flight's departure.",""
14
+ "At which airports is Digi Yatra available?","As of 2023, Digi Yatra is available at several major airports in India including Delhi, Bengaluru, Varanasi, and Hyderabad. The service is being rolled out to more airports in phases.",""
15
+ "Can I use Digi Yatra for international flights?","As of 2023, Digi Yatra is only available for domestic flights in India. The service for international flights is on the roadmap and will be implemented in phases.",""
16
+ "What do I need to use Digi Yatra at the airport?","To use Digi Yatra at the airport, you need to have the Digi Yatra app installed on your smartphone with your digital identity created. You also need to upload your boarding pass to the app before your travel.",""
17
+ "How do I add my minor child to my Digi Yatra account?","To add a minor child to your Digi Yatra account, you first need to create your own account. Then, on the app's home screen, click on 'Add Credentials', then 'Add minor Credentials'. Follow the process to create the child's identity credentials. You can then add their boarding pass similarly to yours.",""
18
+ "What if I face issues with the Digi Yatra app?","If you face any issues with the Digi Yatra app, you can contact their customer support at customercare@digiyatrafoundation.com. It's helpful to include details about your device model, operating system version, and app version when reporting issues.",""
19
+ "Who operates Digi Yatra?","Digi Yatra is operated by the Digi Yatra Foundation, a not-for-profit company under section 8 of the Companies Act 2013. It is an industry-led initiative promoted by the Ministry of Civil Aviation.",""
20
+ "How is passenger consent handled in Digi Yatra?","Digi Yatra takes passenger consent at various stages. For app-based enrollment, consent is taken before creating identity credentials and before sharing these credentials. For 'day of travel' enrollment at airports, consent is taken before capturing the passenger's face.",""
21
+ "Can I delete my Digi Yatra data?","Yes, you can delete your Digi Yatra data by uninstalling the app from your phone. Since no data is stored centrally, uninstalling the app effectively removes all your data from the system.",""
22
+ "Is Digi Yatra available for all airlines?","Digi Yatra is designed to work with all airlines operating domestic flights in India. However, the integration may vary between airlines and airports. It's best to check with your specific airline or airport for the most up-to-date information.",""
23
+ "What happens if the Digi Yatra system is down at the airport?","If the Digi Yatra system is down at the airport, you can use the regular manual processes for identity verification and boarding. The manual processes are always available as an alternative.",""
24
+ "How does Digi Yatra ensure data privacy?","Digi Yatra ensures data privacy by not storing any personally identifiable information in a central database. All your data is stored only on your phone. The data shared with airports for verification is deleted within 24 hours of your flight's departure.",""
25
+ "Can I use Digi Yatra if I don't have an Aadhaar card?","Currently, Digi Yatra primarily uses Aadhaar for identity verification. However, they are working on incorporating other forms of ID, including passports, in the future.",""
26
+ "Does Digi Yatra track my travel history?","No, Digi Yatra does not track or store your travel history centrally. Any travel information is only stored locally on your phone and can be deleted by uninstalling the app.",""
27
+ "How does Digi Yatra handle data security?","Digi Yatra handles data security by implementing 'Privacy by Design' principles. They do not store any central database of user information, minimizing the risk of data breaches. The system is regularly audited by CERT-In empaneled agencies.",""
28
+ "Enrol on the Digi Yatra App using Aadhaar for a hassle-free airport journey","https://www.youtube.com/watch?v=y6xTGrpfAGs"
29
+ "Enrol on the Digi Yatra App using DigiLocker","https://www.youtube.com/watch?v=7z7iLNSkFg4"
30
+ "Enrol on the Digi Yatra App in 3 Simple Steps With Digi Yatra, you can register in just three easy steps using DigiLocker or Aadhaar.
31
+ Step 1: Register on the App
32
+ Step 2: Verify and create your credentials
33
+ Step 3: Share boarding details","https://www.youtube.com/watch?v=-nlJBCwg0nM"
34
+ "SELF-SOVEREIGN IDENTITY | DATA SAFETY WITH DIGI YATRA","https://www.youtube.com/watch?v=Wlc6iqgwQDU"
35
+ "HOW TO ADD A DEPENDENT/MINOR | DIGI YATRA MOBILE APP Traveling with family just got easier! 🌟 Learn how to add dependents or minors to your Digi Yatra app for seamless travel experiences. Watch now and make your next family trip a breeze!","https://www.youtube.com/watch?v=j2lw7LljME0"
36
+ "Does Digi Yatra use centralized data storage?","No, Digi Yatra does not use centralized storage; all personal information is saved in the passenger’s own devices."
37
+ "Where is a passenger's personal information stored in Digi Yatra?","A passenger's personal information is stored in the mobile wallet of the traveler’s smartphone."
38
+ "How does Digi Yatra ensure data privacy?","Digi Yatra Central Ecosystem (DYCE) is built on the principles of privacy by design and default, with no centralized storage of Personally Identifiable Information (PII). Data is encrypted, shared only with the departure airport, and purged 24 hours after flight departure."
39
+ "Who manages the Digi Yatra Central Ecosystem?","The Digi Yatra Central Ecosystem is managed by Digi Yatra Foundation, a Not-For-Profit company established under Section 8 of the Companies Act, 2013."
40
+ "Is Digi Yatra subject to the Right to Information (RTI) Act?","No, Digi Yatra Foundation does not come under the ambit of the Right to Information (RTI) Act."
41
+ "How is data security maintained in Digi Yatra?","Digi Yatra processes are audited and certified by CERT-In empanelled agencies to ensure adherence to data privacy and security standards."
42
+ "How are Digi Yatra guidelines issued?","Digi Yatra Guidelines are issued by the Directorate General of Civil Aviation (DGCA) through Aeronautical Information Circular (AIC) No. 09/2022 dated April 18, 2022."
43
+ "How does the Digi Yatra app work for passengers?","Passengers register their details on the Digi Yatra app using Aadhaar-based validation and a self-image capture. The boarding pass is scanned, and credentials are shared with the airport. At the airport e-gate, the boarding pass is scanned, and facial recognition validates the passenger's identity and travel documents."
44
+ "Is Digi Yatra mandatory for passengers?","No, Digi Yatra is purely voluntary for air passengers. Data is collected only with the consent of passengers."
45
+ "At how many airports is Digi Yatra currently available for domestic passengers?","Digi Yatra is currently available at 13 airports for domestic passengers."
46
+ "Which airports currently have Digi Yatra implemented?","Delhi, Bengaluru, Varanasi, Hyderabad, Kolkata, Vijayawada, Pune, Mumbai, Cochin, Ahmedabad, Lucknow, Jaipur, and Guwahati."
47
+ "How many additional airports will Digi Yatra be introduced to this year?","The government plans to roll out Digi Yatra at 14 more airports this year."
48
+ "Which airports are scheduled to receive Digi Yatra in the first phase?","Chennai, Bhubaneshwar, Coimbatore, Dabolim, Mopa Goa, Indore, Bagdogra, Chandigarh, Ranchi, Nagpur, Patna, Raipur, Srinagar, and Vishakhapatnam."
49
+ "How many airports will have Digi Yatra by the end of 2024?","By the end of 2024, Digi Yatra will be available at 38 airports."
50
+ "When was Digi Yatra introduced?","Digi Yatra was introduced in December 2022."
51
+ "What percentage of India's domestic air passenger traffic is handled by airports with Digi Yatra?","Currently, 13 airports handle around 85 percent of the country's domestic air passenger traffic with Digi Yatra."
52
+ "What are the future plans for Digi Yatra in 2025?","Digi Yatra will be implemented at 11 more airports in 2025, and e-passport based enrollment will be introduced to allow foreign citizens to use the facility."
53
+ "How has the number of Digi Yatra app users changed from December 2022 to November 2023?","There has been a significant growth in the total number of Digi Yatra app users during this period."
54
+ "How does Digi Yatra benefit passengers at the airport?","Digi Yatra reduces processing time at entry and boarding gates, allowing passengers to access terminals and boarding gates seamlessly using facial recognition technology."
55
+ "Which organization is the nodal body for Digi Yatra?","Digi Yatra Foundation is the nodal body for Digi Yatra."
56
+ "How does Digi Yatra handle data after flight departure?","Data is purged from the system 24 hours after the departure of the flight."
57
+ "Is Digi Yatra available for foreign citizens?","The government plans to make Digi Yatra available for foreign citizens by implementing e-passport based enrollment."
58
+ "How has Digi Yatra been received at Pune Airport?","Over 1 million passengers have used Digi Yatra at Pune Airport as of January 31, 2024, with around 57 percent of passengers preferring the service."
59
+ "What improvements have been seen since the launch of Digi Yatra at Guwahati Airport?","The introduction of Digi Yatra at Guwahati Airport has transformed the passenger experience, leading to a surge in usage and positive feedback."
60
+ "What percentage of passengers used Digi Yatra services recently according to MOCA advisory?","At least 10 percent of passengers are expected to use Digi Yatra services, with recent usage increasing to 11.9 percent."
61
+ "Where are Digi Yatra services available at Guwahati Airport?","Digi Yatra services are available at departure gates D-10 and D-7, entry into the terminal, and all boarding gates."
62
+ "When was Digi Yatra officially launched at Guwahati Airport?","Digi Yatra was officially inaugurated at Guwahati Airport in August 2023."
63
+ "What benefits did Digi Yatra bring to Pune Airport?","Digi Yatra allowed passengers to access the terminal in seconds without queuing for check-in, increasing the number of passengers using the service."
64
+ "How does Digi Yatra handle the boarding process?","At the airport e-gate, passengers scan their bar-coded boarding pass and undergo facial recognition to validate their identity and travel documents before entering through the e-gate."
65
+ "Who informed the Rajya Sabha about Digi Yatra's data storage practices?","The Civil Aviation Ministry informed the Rajya Sabha about Digi Yatra's data storage practices."
66
+ "What is the role of CERT-In empanelled agencies in Digi Yatra?","CERT-In empanelled agencies audit and certify Digi Yatra processes to ensure data privacy and security standards are met."
67
+ "Is Digi Yatra considered under the Right to Information (RTI) Act?","No, because Digi Yatra Foundation is a Not-For-Profit company established under Section 8 of the Companies Act, 2013."
68
+ "How is personal information shared in Digi Yatra?","Personal information is shared with the departure airport in an encrypted format."
69
+ "What technology underpins Digi Yatra's boarding system?","Facial Recognition Technology (FRT) is used for biometric boarding in Digi Yatra."
70
+ "How does Digi Yatra enhance the passenger experience at airports?","By enabling contactless and seamless movement through various checkpoints using facial recognition, reducing wait times and eliminating the need for physical boarding passes."
71
+ "What milestone did Pune Airport achieve with Digi Yatra on January 31, 2024?","Pune Airport reached the milestone of 1 million passengers traveling through Digi Yatra."
72
+ "How did passengers initially respond to Digi Yatra at Pune Airport?","Initially, passenger response was very low, and some airlines did not support the service."
73
+ "How did the adoption of Digi Yatra at Pune Airport change over time?","Over time, both passengers and various airlines started preferring Digi Yatra, increasing usage significantly."
74
+ "What actions did Civil Aviation Minister Jyotiraditya Scindia announce regarding Digi Yatra?","He announced that Digi Yatra will be available at 25 more airports in 2024 and reiterated that the service is voluntary for passengers."
75
+ "How does Digi Yatra handle the boarding pass?","Passengers scan their bar-coded boarding pass, which is then validated through facial recognition at the e-gate."
76
+ "Can passengers still follow normal procedures after using Digi Yatra?","Yes, passengers still follow normal procedures to clear security and board the aircraft after using Digi Yatra."
77
+ "What is Digi Yatra?","Digi Yatra is a Ministry of Civil Aviation, Govt. of India led initiative to make air traveller's/ passenger's journey seamless, hassle-free and Health-Risk-Free. The Digi Yatra process uses the single token of face biometrics to digitally validate the Identity, Travel, Health or any other data that is needed for the purpose of enabling air travel."
78
+ "What is the solution based on and is my data safe?","The Solution is built on W3C standards using Self Sovereign Identity (SSI), Verifiable Credentials (VC), Decentralized Identifiers (DIDs) and uses a Distributed Ledger for decentralised layer of trust between the various participants of the ecosystem."
79
+ "Do I have to Register for every travel?","Registration/ enrolment to the DigiYatra App is a one-time process. You don't have to register every time you travel. For each new travel instance, simply add the new travel document and share your credentials prior to your travel."
80
+ "Why can't we process international passengers?","Processing passengers on International flights is on the roadmap of the Digi Yatra Central Ecosystem and will be taken up in a phased manner."
81
+ "How about passengers with turban?","The Biometric algorithm extracts the template for a face match using certain points on the face. Wearing a turban does not impact this validation."
82
+ "How do you process if I am transiting?","Entry check and other validations shall remain the same at the departing airport even if passenger is going through Transit/ Transfer at another airport. However, passenger processing at the Transit/ Transfer airports will be taken up at a later date."
83
+ "Why is only Aadhar accepted as the Govt ID?","At this time, the Digi Yatra app accepts only AADHAAR as it has been integrated, however, we plan to integrate additional government issued identity documents in the future."
84
+
limit_tokens.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ import tiktoken
4
+ from langchain_core.messages import BaseMessage, ToolMessage, HumanMessage, AIMessage, SystemMessage, trim_messages
5
+
6
+
7
+ def str_token_counter(text: str) -> int:
8
+ enc = tiktoken.get_encoding("o200k_base")
9
+ return len(enc.encode(text))
10
+
11
+
12
+ def tiktoken_counter(messages: List[BaseMessage]) -> int:
13
+ """Approximately reproduce https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
14
+
15
+ For simplicity only supports str Message.contents.
16
+ """
17
+ num_tokens = 3 # every reply is primed with <|start|>assistant<|message|>
18
+ tokens_per_message = 3
19
+ tokens_per_name = 1
20
+ for msg in messages:
21
+ if isinstance(msg, HumanMessage):
22
+ role = "user"
23
+ elif isinstance(msg, AIMessage):
24
+ role = "assistant"
25
+ elif isinstance(msg, ToolMessage):
26
+ role = "tool"
27
+ elif isinstance(msg, SystemMessage):
28
+ role = "system"
29
+ else:
30
+ raise ValueError(f"Unsupported messages type {msg.__class__}")
31
+ num_tokens += (
32
+ tokens_per_message
33
+ + str_token_counter(role)
34
+ + str_token_counter(msg.content)
35
+ )
36
+ if msg.name:
37
+ num_tokens += tokens_per_name + str_token_counter(msg.name)
38
+ return num_tokens
39
+
40
+ def convert_to_openai_messages(messages: List[BaseMessage]) -> List[dict]:
41
+ """Convert LangChain messages to OpenAI format."""
42
+ openai_messages = []
43
+
44
+ for msg in messages:
45
+ message_dict = {"content": msg.content}
46
+
47
+ if isinstance(msg, HumanMessage):
48
+ message_dict["role"] = "user"
49
+ elif isinstance(msg, AIMessage):
50
+ message_dict["role"] = "assistant"
51
+ elif isinstance(msg, SystemMessage):
52
+ message_dict["role"] = "system"
53
+ elif isinstance(msg, ToolMessage):
54
+ message_dict["role"] = "tool"
55
+ else:
56
+ raise ValueError(f"Unsupported message type: {msg.__class__}")
57
+
58
+ if msg.name:
59
+ message_dict["name"] = msg.name
60
+
61
+ openai_messages.append(message_dict)
62
+
63
+ return openai_messages
64
+
65
+ def trim_messages_openai(messages: List[BaseMessage]) -> List[dict]:
66
+ """Trim LangChain messages and convert to OpenAI format."""
67
+
68
+ trimmed_messages = trim_messages(
69
+ messages,
70
+ token_counter=tiktoken_counter,
71
+ strategy="last",
72
+ max_tokens=45,
73
+ start_on="human",
74
+ end_on=("human", "tool"),
75
+ include_system=True,
76
+ )
77
+ openai_format_messages = convert_to_openai_messages(trimmed_messages)
78
+
79
+ return openai_format_messages
80
+
81
+ # Test
82
+ # messages = [SystemMessage(content="You are a helpful assistant."), HumanMessage(query)]
83
+ # openai_format_messages = trim_messages_openai(messages)
llamaindex.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # %pip install llama-index llama-index-vector-stores-lancedb
2
+ # %pip install lancedb==0.6.13 #Only required if the above cell installs an older version of lancedb (pypi package may not be released yet)
3
+ # %pip install llama-index-embeddings-fastembed
4
+ # pip install llama-index-readers-file
5
+ from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex
6
+ from llama_index.vector_stores.lancedb import LanceDBVectorStore
7
+ from llama_index.embeddings.fastembed import FastEmbedEmbedding
8
+
9
+ # Configure global settings
10
+ Settings.embed_model = FastEmbedEmbedding(model_name="BAAI/bge-small-en-v1.5")
11
+
12
+ # Setup LanceDB vector store
13
+ vector_store = LanceDBVectorStore(
14
+ uri="./lancedb",
15
+ mode="overwrite",
16
+ query_type="vector"
17
+ )
18
+
19
+ # Load your documents
20
+ documents = SimpleDirectoryReader("D:\DEV\LIZMOTORS\LANGCHAIN\digiyatrav2\chatbot\data").load_data()
21
+
22
+ # Create the index
23
+ index = VectorStoreIndex.from_documents(
24
+ documents,
25
+ vector_store=vector_store
26
+ )
27
+
28
+ # Create a retriever
29
+ retriever = index.as_retriever()
30
+ response = retriever.retrieve("Your query here")
31
+ print(response)
llm_observability_v2.db ADDED
Binary file (49.2 kB). View file
 
observability.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: llm_observability.py
2
+ import sqlite3
3
+ import json
4
+ from datetime import datetime
5
+ from typing import Dict, Any, List, Optional, Callable
6
+ import logging
7
+ import functools
8
+
9
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def log_execution(func: Callable) -> Callable:
13
+ @functools.wraps(func)
14
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
15
+ logger.info(f"Executing {func.__name__}")
16
+ try:
17
+ result = func(*args, **kwargs)
18
+ logger.info(f"{func.__name__} completed successfully")
19
+ return result
20
+ except Exception as e:
21
+ logger.error(f"Error in {func.__name__}: {e}")
22
+ raise
23
+ return wrapper
24
+
25
+
26
+ class LLMObservabilityManager:
27
+ def __init__(self, db_path: str = "llm_observability_v2.db"):
28
+ self.db_path = db_path
29
+ self.create_table()
30
+
31
+ def create_table(self):
32
+ with sqlite3.connect(self.db_path) as conn:
33
+ cursor = conn.cursor()
34
+ cursor.execute('''
35
+ CREATE TABLE IF NOT EXISTS llm_observations (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ conversation_id TEXT,
38
+ created_at DATETIME,
39
+ status TEXT,
40
+ request TEXT,
41
+ response TEXT,
42
+ model TEXT,
43
+ prompt_tokens INTEGER,
44
+ completion_tokens INTEGER,
45
+ total_tokens INTEGER,
46
+ cost FLOAT,
47
+ latency FLOAT,
48
+ user TEXT
49
+ )
50
+ ''')
51
+
52
+ def insert_observation(self, response: str, conversation_id: str, status: str, request: str, model: str, prompt_tokens: int,completion_tokens: int, total_tokens: int, cost: float, latency: float, user: str):
53
+ created_at = datetime.now()
54
+
55
+ with sqlite3.connect(self.db_path) as conn:
56
+ cursor = conn.cursor()
57
+ cursor.execute('''
58
+ INSERT INTO llm_observations
59
+ (conversation_id, created_at, status, request, response, model, prompt_tokens, completion_tokens,total_tokens, cost, latency, user)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
61
+ ''', (
62
+ conversation_id,
63
+ created_at,
64
+ status,
65
+ request,
66
+ response,
67
+ model,
68
+ prompt_tokens,
69
+ completion_tokens,
70
+ total_tokens,
71
+ cost,
72
+ latency,
73
+ user
74
+ ))
75
+
76
+ def get_observations(self, conversation_id: Optional[str] = None) -> List[Dict[str, Any]]:
77
+ with sqlite3.connect(self.db_path) as conn:
78
+ cursor = conn.cursor()
79
+ if conversation_id:
80
+ cursor.execute('SELECT * FROM llm_observations WHERE conversation_id = ? ORDER BY created_at', (conversation_id,))
81
+ else:
82
+ cursor.execute('SELECT * FROM llm_observations ORDER BY created_at')
83
+ rows = cursor.fetchall()
84
+
85
+ column_names = [description[0] for description in cursor.description]
86
+ return [dict(zip(column_names, row)) for row in rows]
87
+
88
+ def get_all_observations(self) -> List[Dict[str, Any]]:
89
+ return self.get_observations()
90
+
91
+ def get_all_unique_conversation_observations(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
92
+ with sqlite3.connect(self.db_path) as conn:
93
+ cursor = conn.cursor()
94
+ # Get the latest observation for each unique conversation_id
95
+ query = '''
96
+ SELECT * FROM llm_observations o1
97
+ WHERE created_at = (
98
+ SELECT MAX(created_at)
99
+ FROM llm_observations o2
100
+ WHERE o2.conversation_id = o1.conversation_id
101
+ )
102
+ ORDER BY created_at DESC
103
+ '''
104
+ if limit is not None:
105
+ query += f' LIMIT {limit}'
106
+
107
+ cursor.execute(query)
108
+ rows = cursor.fetchall()
109
+
110
+ column_names = [description[0] for description in cursor.description]
111
+ return [dict(zip(column_names, row)) for row in rows]
112
+
113
+ ## OBSERVABILITY
114
+ from uuid import uuid4
115
+ import csv
116
+ from io import StringIO
117
+ from fastapi import APIRouter, HTTPException
118
+ from pydantic import BaseModel
119
+ from starlette.responses import StreamingResponse
120
+
121
+
122
+
123
+ router = APIRouter(
124
+ prefix="/observability",
125
+ tags=["observability"]
126
+ )
127
+
128
+ class ObservationResponse(BaseModel):
129
+ observations: List[Dict]
130
+
131
+ def create_csv_response(observations: List[Dict]) -> StreamingResponse:
132
+ def iter_csv(data):
133
+ output = StringIO()
134
+ writer = csv.DictWriter(output, fieldnames=data[0].keys() if data else [])
135
+ writer.writeheader()
136
+ for row in data:
137
+ writer.writerow(row)
138
+ output.seek(0)
139
+ yield output.read()
140
+
141
+ headers = {
142
+ 'Content-Disposition': 'attachment; filename="observations.csv"'
143
+ }
144
+ return StreamingResponse(iter_csv(observations), media_type="text/csv", headers=headers)
145
+
146
+
147
+ @router.get("/last-observations/{limit}")
148
+ async def get_last_observations(limit: int = 10, format: str = "json"):
149
+ observability_manager = LLMObservabilityManager()
150
+
151
+ try:
152
+ # Get all observations, sorted by created_at in descending order
153
+ all_observations = observability_manager.get_observations()
154
+ all_observations.sort(key=lambda x: x['created_at'], reverse=True)
155
+
156
+ # Get the last conversation_id
157
+ if all_observations:
158
+ last_conversation_id = all_observations[0]['conversation_id']
159
+
160
+ # Filter observations for the last conversation
161
+ last_conversation_observations = [
162
+ obs for obs in all_observations
163
+ if obs['conversation_id'] == last_conversation_id
164
+ ][:limit]
165
+
166
+ if format.lower() == "csv":
167
+ return create_csv_response(last_conversation_observations)
168
+ else:
169
+ return ObservationResponse(observations=last_conversation_observations)
170
+ else:
171
+ if format.lower() == "csv":
172
+ return create_csv_response([])
173
+ else:
174
+ return ObservationResponse(observations=[])
175
+ except Exception as e:
176
+ raise HTTPException(status_code=500, detail=f"Failed to retrieve observations: {str(e)}")
observability_router.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## OBSERVABILITY
2
+ from uuid import uuid4
3
+ import csv
4
+ from io import StringIO
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi.responses import StreamingResponse
7
+ from pydantic import BaseModel
8
+ from typing import List, Dict, Optional
9
+ from observability import LLMObservabilityManager
10
+
11
+
12
+ router = APIRouter(
13
+ prefix="/observability",
14
+ tags=["observability"]
15
+ )
16
+
17
+
18
+
19
+ class ObservationResponse(BaseModel):
20
+ observations: List[Dict]
21
+
22
+ def create_csv_response(observations: List[Dict]) -> StreamingResponse:
23
+ def iter_csv(data):
24
+ output = StringIO()
25
+ writer = csv.DictWriter(output, fieldnames=data[0].keys() if data else [])
26
+ writer.writeheader()
27
+ for row in data:
28
+ writer.writerow(row)
29
+ output.seek(0)
30
+ yield output.read()
31
+
32
+ headers = {
33
+ 'Content-Disposition': 'attachment; filename="observations.csv"'
34
+ }
35
+ return StreamingResponse(iter_csv(observations), media_type="text/csv", headers=headers)
36
+
37
+
38
+ @router.get("/last-observations/{limit}")
39
+ async def get_last_observations(limit: int = 10, format: str = "json"):
40
+ observability_manager = LLMObservabilityManager()
41
+
42
+ try:
43
+ # Get all observations, sorted by created_at in descending order
44
+ all_observations = observability_manager.get_observations()
45
+ all_observations.sort(key=lambda x: x['created_at'], reverse=True)
46
+
47
+ # Get the last conversation_id
48
+ if all_observations:
49
+ last_conversation_id = all_observations[0]['conversation_id']
50
+
51
+ # Filter observations for the last conversation
52
+ last_conversation_observations = [
53
+ obs for obs in all_observations
54
+ if obs['conversation_id'] == last_conversation_id
55
+ ][:limit]
56
+
57
+ if format.lower() == "csv":
58
+ return create_csv_response(last_conversation_observations)
59
+ else:
60
+ return ObservationResponse(observations=last_conversation_observations)
61
+ else:
62
+ if format.lower() == "csv":
63
+ return create_csv_response([])
64
+ else:
65
+ return ObservationResponse(observations=[])
66
+ except Exception as e:
67
+ raise HTTPException(status_code=500, detail=f"Failed to retrieve observations: {str(e)}")
68
+
69
+ @router.get("/all-unique-observations")
70
+ async def get_all_unique_observations(limit: Optional[int] = None):
71
+ observability_manager = LLMObservabilityManager()
72
+ return ObservationResponse(observations=observability_manager.get_all_unique_conversation_observations(limit))
73
+
prompts.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FOLLOWUP_DIGIYATRA_PROMPT = """
2
+ You are a helpful, assistant tasked to assist digiyatra users, who can create interactive buttons and markdown responses. Provide youtube links to the user if relevant links are given in the context.
3
+ If the user request needs further clarification, provide clarifying questions using <interact> to assist the user.
4
+ Else respond with a helpful answer using <response>.
5
+ The options in <interact> tags will be rendered as buttons so that user can interact with it. Hence make sure to use it for engaging with the user with Next Steps, followup questions, quizzes etc. whichever appropriate
6
+ Your output format: # Use the following <response>,<interact> tags in any order as many times required.
7
+ <response>response to user request formatted using markdown</response>
8
+ <interact>
9
+ questions:
10
+ - text: [First interaction question]
11
+ options:
12
+ - [Option 1]
13
+ - [Option 2]
14
+ - [Option 3]
15
+ - [Option 4 (if needed)]
16
+ - text: [Second interaction question]
17
+ options:
18
+ - [Option 1]
19
+ - [Option 2]
20
+ - [Option 3]
21
+ # Add more questions as needed
22
+ # make sure this section is in valid YAML format
23
+ </interact>
24
+
25
+ Refer the FAQ Context for ANSWERING the user query.
26
+ """
rag_routerv2.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Depends, HTTPException, UploadFile, File
2
+ import pandas as pd
3
+ import lancedb
4
+ from functools import cached_property, lru_cache
5
+ from pydantic import Field, BaseModel
6
+ from typing import Optional, Dict, List, Annotated, Any
7
+ from fastapi import APIRouter
8
+ import uuid
9
+ import io
10
+ from io import BytesIO
11
+ import csv
12
+
13
+ # LlamaIndex imports
14
+ from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex
15
+ from llama_index.vector_stores.lancedb import LanceDBVectorStore
16
+ from llama_index.embeddings.fastembed import FastEmbedEmbedding
17
+ from llama_index.core import StorageContext, load_index_from_storage
18
+ import json
19
+ import os
20
+ import shutil
21
+
22
+ router = APIRouter(
23
+ prefix="/rag",
24
+ tags=["rag"]
25
+ )
26
+
27
+ # Configure global LlamaIndex settings
28
+ Settings.embed_model = FastEmbedEmbedding(model_name="BAAI/bge-small-en-v1.5")
29
+ tables_file_path = './data/tables.json'
30
+
31
+ # Database connection dependency
32
+ @lru_cache()
33
+ def get_db_connection(db_path: str = "./lancedb/dev"):
34
+ return lancedb.connect(db_path)
35
+
36
+ # Pydantic models
37
+ class CreateTableResponse(BaseModel):
38
+ table_id: str
39
+ message: str
40
+ status: str
41
+
42
+ class QueryTableResponse(BaseModel):
43
+ results: Dict[str, Any]
44
+ total_results: int
45
+
46
+
47
+ @router.post("/create_table", response_model=CreateTableResponse)
48
+ async def create_embedding_table(
49
+ user_id: str,
50
+ files: List[UploadFile] = File(...),
51
+ table_id: Optional[str] = None
52
+ ) -> CreateTableResponse:
53
+ """Create a table and load embeddings from uploaded files using LlamaIndex."""
54
+ allowed_extensions = {".pdf", ".docx", ".csv", ".txt", ".md"}
55
+ for file in files:
56
+ if file.filename is None:
57
+ raise HTTPException(status_code=400, detail="File must have a valid name.")
58
+ file_extension = os.path.splitext(file.filename)[1].lower()
59
+ if file_extension not in allowed_extensions:
60
+ raise HTTPException(
61
+ status_code=400,
62
+ detail=f"File type {file_extension} is not allowed. Supported file types are: {', '.join(allowed_extensions)}."
63
+ )
64
+
65
+ if table_id is None:
66
+ table_id = str(uuid.uuid4())
67
+ table_name = table_id #f"{user_id}__table__{table_id}"
68
+
69
+ # Create a directory for the uploaded files
70
+ directory_path = f"./data/{table_name}"
71
+ os.makedirs(directory_path, exist_ok=True)
72
+
73
+ # Save each uploaded file to the data directory
74
+ for file in files:
75
+ file_path = os.path.join(directory_path, file.filename)
76
+ with open(file_path, "wb") as buffer:
77
+ shutil.copyfileobj(file.file, buffer)
78
+
79
+ # Store user_id and table_name in a JSON file
80
+ try:
81
+ tables_file_path = './data/tables.json'
82
+ os.makedirs(os.path.dirname(tables_file_path), exist_ok=True)
83
+ # Load existing tables or create a new file if it doesn't exist
84
+ try:
85
+ with open(tables_file_path, 'r') as f:
86
+ tables = json.load(f)
87
+ except (FileNotFoundError, json.JSONDecodeError):
88
+ tables = {}
89
+
90
+ # Update the tables dictionary
91
+ if user_id not in tables:
92
+ tables[user_id] = []
93
+ if table_name not in tables[user_id]:
94
+ tables[user_id].append(table_name)
95
+
96
+ # Write the updated tables back to the JSON file
97
+ with open(tables_file_path, 'w') as f:
98
+ json.dump(tables, f)
99
+
100
+ except Exception as e:
101
+ raise HTTPException(status_code=500, detail=f"Failed to update tables file: {str(e)}")
102
+ try:
103
+ # Setup LanceDB vector store
104
+ vector_store = LanceDBVectorStore(
105
+ uri="./lancedb/dev",
106
+ table_name=table_name,
107
+ # mode="overwrite",
108
+ # query_type="vector"
109
+ )
110
+
111
+ # Load documents using SimpleDirectoryReader
112
+ documents = SimpleDirectoryReader(directory_path).load_data()
113
+
114
+ # Create the index
115
+ index = VectorStoreIndex.from_documents(
116
+ documents,
117
+ vector_store=vector_store
118
+ )
119
+ index.storage_context.persist(persist_dir=f"./lancedb/index/{table_name}")
120
+
121
+ return CreateTableResponse(
122
+ table_id=table_id,
123
+ message=f"Table created and documents indexed successfully",
124
+ status="success"
125
+ )
126
+
127
+ except Exception as e:
128
+ raise HTTPException(status_code=500, detail=f"Table creation failed: {str(e)}")
129
+
130
+ @router.post("/query_table/{table_id}", response_model=QueryTableResponse)
131
+ async def query_table(
132
+ table_id: str,
133
+ query: str,
134
+ user_id: str,
135
+ #db: Annotated[Any, Depends(get_db_connection)],
136
+ limit: Optional[int] = 10
137
+ ) -> QueryTableResponse:
138
+ """Query the database table using LlamaIndex."""
139
+ try:
140
+ table_name = table_id #f"{user_id}__table__{table_id}"
141
+
142
+ # load index and retriever
143
+ storage_context = StorageContext.from_defaults(persist_dir=f"./lancedb/index/{table_name}")
144
+ index = load_index_from_storage(storage_context)
145
+ retriever = index.as_retriever(similarity_top_k=limit)
146
+
147
+ # Get response
148
+ response = retriever.retrieve(query)
149
+
150
+ # Format results
151
+ results = [{
152
+ 'text': node.text,
153
+ 'score': node.score
154
+ } for node in response]
155
+
156
+ return QueryTableResponse(
157
+ results={'data': results},
158
+ total_results=len(results)
159
+ )
160
+
161
+ except Exception as e:
162
+ raise HTTPException(status_code=500, detail=f"Query failed: {str(e)}")
163
+
164
+ @router.get("/get_tables/{user_id}")
165
+ async def get_tables(user_id: str):
166
+ """Get all tables for a user."""
167
+
168
+ tables_file_path = './data/tables.json'
169
+ try:
170
+ # Load existing tables from the JSON file
171
+ with open(tables_file_path, 'r') as f:
172
+ tables = json.load(f)
173
+
174
+ # Retrieve tables for the specified user
175
+ user_tables = tables.get(user_id, [])
176
+ return user_tables
177
+
178
+ except (FileNotFoundError, json.JSONDecodeError):
179
+ return [] # Return an empty list if the file doesn't exist or is invalid
180
+ except Exception as e:
181
+ raise HTTPException(status_code=500, detail=f"Failed to retrieve tables: {str(e)}")
182
+
183
+ @router.get("/health")
184
+ async def health_check():
185
+ return {"status": "healthy"}
requirements.txt CHANGED
@@ -1,14 +1,4 @@
1
- fastapi[standard]
2
- uvicorn
3
- openai
4
- psycopg2-binary
5
- tiktoken
6
- requests
7
- beautifulsoup4
8
- fastapi-cache2
9
- PyYAML
10
- psycopg2-binary
11
- pandas
12
- txtai
13
- llama-parse
14
- fast-langdetect
 
1
+ llama-index-core
2
+ llama-index-readers-file
3
+ llama-index-vector-stores-lancedb
4
+ llama_index.vector_stores
 
 
 
 
 
 
 
 
 
 
utils.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ def parse_followup_and_tools(input_text):
4
+ # Remove extra brackets and excess quotes
5
+ cleaned_text = re.sub(r'\[|\]|"+', ' ', input_text)
6
+
7
+ # Extract response content
8
+ response_pattern = re.compile(r'<response>(.*?)</response>', re.DOTALL)
9
+ response_parts = response_pattern.findall(cleaned_text)
10
+ combined_response = ' '.join(response_parts)
11
+
12
+ # Normalize spaces in the combined response
13
+ combined_response = ' '.join(combined_response.split())
14
+
15
+ parsed_interacts = []
16
+ parsed_tools = []
17
+
18
+ # Parse interacts and tools
19
+ blocks = re.finditer(r'<(interact|tools?)(.*?)>(.*?)</\1>', cleaned_text, re.DOTALL)
20
+ for block in blocks:
21
+ block_type, _, content = block.groups()
22
+ content = content.strip()
23
+
24
+ if block_type == 'interact':
25
+ question_blocks = re.split(r'\s*-\s*text:', content)[1:]
26
+ for qblock in question_blocks:
27
+ parts = re.split(r'\s*options:\s*', qblock, maxsplit=1)
28
+ if len(parts) == 2:
29
+ question = ' '.join(parts[0].split()) # Normalize spaces
30
+ options = [' '.join(opt.split()) for opt in re.split(r'\s*-\s*', parts[1]) if opt.strip()]
31
+ parsed_interacts.append({'question': question, 'options': options})
32
+
33
+ elif block_type.startswith('tool'): # This will match both 'tool' and 'tools'
34
+ tool_match = re.search(r'text:\s*(.*?)\s*options:\s*-\s*(.*)', content, re.DOTALL)
35
+ if tool_match:
36
+ tool_name = ' '.join(tool_match.group(1).split()) # Normalize spaces
37
+ option = ' '.join(tool_match.group(2).split()) # Normalize spaces
38
+ parsed_tools.append({'name': tool_name, 'input': option})
39
+
40
+ return combined_response, parsed_interacts, parsed_tools