44 |
45 | | Type |
46 | Langage |
47 | Librairie |
48 | Adaptateurs |
49 |
50 |
51 | | Base de données |
52 | Ruby/Rails |
53 | ActiveRecord |
54 | MySQL, PostgreSQL, SQLite |
55 |
56 |
57 | | File de messages |
58 | Python/Django |
59 | Celery |
60 | RabbitMQ, Beanstalkd, Redis |
61 |
62 |
63 | | Cache |
64 | Ruby/Rails |
65 | ActiveSupport::Cache |
66 | Mémoire, système de fichiers, Memcached |
67 |
68 |
69 |
70 | Les développeurs trouvent parfois agréable d'utiliser des services externes légers dans leur environnement local, alors qu'un service externe plus sérieux et robuste est utilisé en production. Par exemple, utiliser SQLite en local, et PostgreSQL en production; ou bien, durant le développement, mettre les données en cache dans la mémoire des processus locaux, et utiliser Memcached en production.
71 |
72 | **Les développeurs des applications 12 facteurs résistent au besoin d'utiliser des services externes différents entre le développement local et la production**, même lorsque les adaptateurs permettent d'abstraire en théorie beaucoup de différences entre les services externes. Les différences entre les services externes signifient que de petites incompatibilités surviennent, ce qui va faire que du code qui fonctionnait et qui passait les tests durant le développement ou la validation ne fonctionnera pas en production. Ce type d'erreurs crée de la friction en défaveur du déploiement continu. Le coût de cette friction et son impact négatif sur le déploiement continu est extrêmement élevé lorsqu'il est cumulé sur toute la vie de l'application.
73 |
74 | Les services locaux légers sont moins attirants aujourd'hui qu'ils ne l'étaient autrefois. Les services externes modernes tels que Memcached, PostgreSQL, et RabbitMQ ne sont pas difficiles à installer et à faire fonctionner grâce aux systèmes de paquets modernes comme [Homebrew](http://mxcl.github.com/homebrew/) et [apt-get](https://help.ubuntu.com/community/AptGet/Howto). Autre possibilité, des outils de provisionnement comme [Chef](http://www.opscode.com/chef/) et [Puppet](http://docs.puppetlabs.com/), combinés à des environnements virtuels légers comme [Docker](https://www.docker.com/) et [Vagrant](http://vagrantup.com/) permettent aux développeurs de faire fonctionner des environnements locaux qui reproduisent de très près les environnements de production. Le coût d'installation et d'utilisation de ces systèmes est faible comparé aux bénéfices d'une bonne parité développement/production et du déploiement continu.
75 |
76 | Les adaptateurs à ces différents systèmes externes sont malgré tout utiles, car ils rendent le portage vers de nouveaux services externes relativement indolores. Mais tous les déploiements de l'application (environnement de développement, validation, production) devraient utiliser le même type et la même version de chacun de ces services externes.
77 |
--------------------------------------------------------------------------------
/tests/rag_bedrock/test_rag_langchain.py:
--------------------------------------------------------------------------------
1 |
2 | import numpy as np
3 | import pytest
4 |
5 |
6 | from rag_bedrock.base import LangchainTestRAGHelper
7 |
8 |
9 | @pytest.mark.usefixtures("trulens_prepare",
10 | "bedrock_prepare",
11 | "documents_prepare",
12 | "llm_prepare",
13 | "embeddings_prepare",
14 | "eval_questions_prepare",
15 | "trulens_context_prepare",
16 | "provider_prepare",
17 | "rag_prepare",
18 | "feedbacks_prepare")
19 | class TestRAGLangChainClaude3SonnetTitanEmbedV1(LangchainTestRAGHelper):
20 |
21 | @property
22 | def test_name(self):
23 | return "Langchain_Claude_3_Sonnet_Titan_Embed_V1"
24 |
25 | @property
26 | def model_id(self):
27 | return "anthropic.claude-3-sonnet-20240229-v1:0"
28 |
29 | @property
30 | def embedding_model_id(self):
31 | return "amazon.titan-embed-text-v1"
32 |
33 |
34 | @pytest.mark.usefixtures("trulens_prepare",
35 | "bedrock_prepare",
36 | "documents_prepare",
37 | "llm_prepare",
38 | "embeddings_prepare",
39 | "eval_questions_prepare",
40 | "trulens_context_prepare",
41 | "provider_prepare",
42 | "rag_prepare",
43 | "feedbacks_prepare")
44 | class TestRAGLangChainClaude3SonnetTitanEmbedV2(LangchainTestRAGHelper):
45 |
46 | @property
47 | def test_name(self):
48 | return "Langchain_Claude_3_Sonnet_Titan_Embed_V2"
49 |
50 | @property
51 | def model_id(self):
52 | return "anthropic.claude-3-sonnet-20240229-v1:0"
53 |
54 | @property
55 | def embedding_model_id(self):
56 | return "amazon.titan-embed-text-v2:0"
57 |
58 |
59 | @pytest.mark.usefixtures("trulens_prepare",
60 | "bedrock_prepare",
61 | "documents_prepare",
62 | "llm_prepare",
63 | "embeddings_prepare",
64 | "trulens_context_prepare",
65 | "provider_prepare",
66 | "eval_questions_prepare",
67 | "rag_prepare",
68 | "feedbacks_prepare")
69 | class TestRAGLangChainMistralLargeTitanEmbedV1(LangchainTestRAGHelper):
70 |
71 | @property
72 | def test_name(self):
73 | return "Langchain_Mistral_Large_Titan_Embed_V1"
74 |
75 | @property
76 | def model_id(self):
77 | return "mistral.mistral-large-2402-v1:0"
78 |
79 | @property
80 | def embedding_model_id(self):
81 | return "amazon.titan-embed-text-v1"
82 |
83 |
84 | @pytest.mark.usefixtures("trulens_prepare",
85 | "bedrock_prepare",
86 | "documents_prepare",
87 | "llm_prepare",
88 | "embeddings_prepare",
89 | "trulens_context_prepare",
90 | "provider_prepare",
91 | "eval_questions_prepare",
92 | "rag_prepare",
93 | "feedbacks_prepare")
94 | class TestRAGLangChainMistralLargeTitanEmbedV2(LangchainTestRAGHelper):
95 |
96 | @property
97 | def test_name(self):
98 | return "Langchain_Mistral_Large_Titan_Embed_V2"
99 |
100 | @property
101 | def model_id(self):
102 | return "mistral.mistral-large-2402-v1:0"
103 |
104 | @property
105 | def embedding_model_id(self):
106 | return "amazon.titan-embed-text-v2:0"
107 |
108 |
109 | @pytest.mark.usefixtures("trulens_prepare",
110 | "bedrock_prepare",
111 | "documents_prepare",
112 | "llm_prepare",
113 | "embeddings_prepare",
114 | "trulens_context_prepare",
115 | "provider_prepare",
116 | "eval_questions_prepare",
117 | "rag_prepare",
118 | "feedbacks_prepare")
119 | class TestRAGLangChainMistralLargeTitanEmbedMultiModal(LangchainTestRAGHelper):
120 |
121 | @property
122 | def test_name(self):
123 | return "Langchain_Mistral_Large_Titan_Multimodal"
124 |
125 | @property
126 | def model_id(self):
127 | return "mistral.mistral-large-2402-v1:0"
128 |
129 | @property
130 | def embedding_model_id(self):
131 | return "amazon.titan-embed-image-v1"
132 |
133 |
134 | @pytest.mark.usefixtures("trulens_prepare",
135 | "bedrock_prepare",
136 | "documents_prepare",
137 | "llm_prepare",
138 | "embeddings_prepare",
139 | "trulens_context_prepare",
140 | "provider_prepare",
141 | "eval_questions_prepare",
142 | "rag_prepare",
143 | "feedbacks_prepare")
144 | class TestRAGLangChainMistralLargeCohereEmbedMultiLingual(LangchainTestRAGHelper):
145 |
146 | @property
147 | def test_name(self):
148 | return "Langchain_Mistral_Large_Cohere_Embed"
149 |
150 | @property
151 | def model_id(self):
152 | return "mistral.mistral-large-2402-v1:0"
153 |
154 | @property
155 | def embedding_model_id(self):
156 | return "cohere.embed-multilingual-v3"
157 |
--------------------------------------------------------------------------------
/rag_assistant/shared/rag_prompts.py:
--------------------------------------------------------------------------------
1 | __template__ = """Answer the following questions as best you can. You have access to the following tools:
2 |
3 | {tools}
4 |
5 | Use the following format:
6 |
7 | Question: the input question you must answer
8 | Thought: you should always think about what to do
9 | Action: the action to take, should be one of [{tool_names}]
10 | Action Input: the input to the action
11 | Observation: the result of the action
12 | ... (this Thought/Action/Action Input/Observation can repeat N times)
13 | Thought: I now know the final answer
14 | Final Answer: the final answer to the original input question
15 |
16 | Only use information provided in the context.
17 | Check your output and make sure it conforms!
18 | DO NOT output an action and a final answer at the same time.
19 | NEVER output a final answer if you are still expecting to receive the response of a tool.
20 |
21 | Begin!"""
22 |
23 | __structured_chat_agent__ = '''Respond to the human as helpfully and accurately as possible.
24 | You have access to the following tools:
25 |
26 | {tools}
27 |
28 | Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
29 |
30 | Valid "action" values: "Final Answer" or {tool_names}
31 |
32 | Provide only ONE action per $JSON_BLOB, as shown:
33 |
34 | ```
35 | {{
36 | "action": $TOOL_NAME,
37 | "action_input": $INPUT
38 | }}
39 | ```
40 |
41 | Follow this format:
42 |
43 | Question: input question to answer
44 | Thought: consider previous and subsequent steps
45 | Action:
46 | ```
47 | $JSON_BLOB
48 | ```
49 | Observation: action result
50 | ... (repeat Thought/Action/Observation N times)
51 | Thought: I know what to respond
52 | Action:
53 | ```
54 | {{
55 | "action": "Final Answer",
56 | "action_input": "Final response to human"
57 | }}
58 |
59 | Reminder to ALWAYS respond with a valid json blob of a single action.
60 | Do not respond directly to question. Only use information provided in the context.
61 | Use tools to retrieve relevant information.
62 | DO NOT output an action and a final answer at the same time.
63 | Format is Action:```$JSON_BLOB``` then Observation
64 |
65 | Begin! '''
66 |
67 |
68 | __template2__ = """You are an assistant designed to guide users through a structured risk assessment questionnaire for cloud deployment.
69 | The questionnaire is designed to cover various pillars essential for cloud architecture,
70 | including security, compliance, availability, access methods, data storage, processing, performance efficiency,
71 | cost optimization, and operational excellence.
72 |
73 | For each question, you are to follow the "Chain of Thought" process. This means that for each user's response, you will:
74 |
75 | - Acknowledge the response,
76 | - Reflect on the implications of the choice,
77 | - Identify any risks associated with the selected option,
78 | - Suggest best practices and architecture patterns that align with the user’s selection,
79 | - Guide them to the next relevant question based on their previous answers.
80 |
81 | Your objective is to ensure that by the end of the questionnaire, the user has a clear understanding of the appropriate architecture and services needed for a secure, efficient, and compliant cloud deployment. Remember to provide answers in a simple, interactive, and concise manner.
82 |
83 | Process:
84 |
85 | 1. Begin by introducing the purpose of the assessment and ask the first question regarding data security and compliance.
86 | 2. Based on the response, discuss the chosen level of data security, note any specific risks or requirements,
87 | and recommend corresponding cloud services or architectural patterns.
88 | 3. Proceed to the next question on application availability. Once the user responds,
89 | reflect on the suitability of their choice for their application's criticality and suggest availability configurations.
90 | 4. For questions on access methods and data storage,
91 | provide insights on securing application access points or optimizing data storage solutions.
92 | 5. When discussing performance efficiency,
93 | highlight the trade-offs between performance and cost, and advise on scaling strategies.
94 | 6. In the cost optimization section,
95 | engage in a brief discussion on budgetary constraints and recommend cost-effective cloud resource management.
96 | 7. Conclude with operational excellence,
97 | focusing on automation and monitoring,
98 | and propose solutions for continuous integration and deployment.
99 | 8. After the final question,
100 | summarize the user's choices and their implications for cloud architecture.
101 | 9. Offer a brief closing statement that reassures the user of the assistance provided
102 | and the readiness of their cloud deployment strategy.
103 |
104 | Keep the interactions focused on architectural decisions without diverting to other unrelated topics.
105 | You are not to perform tasks outside the scope of the questionnaire,
106 | such as executing code or accessing external databases.
107 | Your guidance should be solely based on the information provided by the user in the context of the questionnaire.
108 | Always answer in French.
109 | {context}
110 | Question: {question}
111 | Helpful Answer:"""
112 |
113 |
114 | human = '''{input}
115 |
116 | {agent_scratchpad}'''
117 |
--------------------------------------------------------------------------------
/rag_assistant/pages/3_RAG_Admin.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | import json
4 |
5 | from utils.auth import check_password
6 | from langchain_community.vectorstores import OpenSearchVectorSearch
7 |
8 | from utils.constants import DocumentType, ChunkType, Metadata, CollectionType
9 | from utils.utilsdoc import get_store, empty_store, extract_unique_name, get_collection_count, get_metadatas, delete_documents_by_type_and_name
10 | from utils.utilsfile import list_files, delete_file
11 | from utils.config_loader import load_config
12 |
13 |
14 | config = load_config()
15 | app_name = config['DEFAULT']['APP_NAME']
16 | collection_name = config['VECTORDB']['collection_name']
17 |
18 | st.set_page_config(page_title=f"""📄 {app_name} 🤗""", page_icon="📄")
19 |
20 |
21 | def main():
22 | st.title(f"""Gestion des connaissances 📄""")
23 |
24 | # collection_name = st.selectbox("Collection", ["Default", "RAG"])
25 |
26 | count = get_collection_count(collection_name)
27 | if count > 0:
28 | st.write(f"Il y a **{count}** morceaux (chunks) dans la collection '**{collection_name}**'.")
29 | else:
30 | st.write("La collection est vide.")
31 | st.page_link("pages/2_Load_Document.py", label="Charger les connaissances")
32 |
33 | st.subheader("Fichier(s) chargé(s)")
34 |
35 | unique_filenames = extract_unique_name(collection_name, Metadata.FILENAME.value)
36 |
37 | for name in unique_filenames:
38 | st.markdown(f"""- {name}""")
39 |
40 | st.subheader("Sujet(s) disponible(s):")
41 | unique_topic_names = extract_unique_name(collection_name, Metadata.TOPIC.value)
42 | for name in unique_topic_names:
43 | st.markdown(f"""- {name}""")
44 |
45 | # st.subheader("Document Type")
46 | # unique_document_types = extract_unique_name(collection_name, 'document_type')
47 | # for name in unique_document_types:
48 | # st.markdown(f"""- {name}""")
49 |
50 | with st.form("search"):
51 | st.subheader("Chercher dans la Base de Connaissance:")
52 | search = st.text_input("Texte (*)")
53 |
54 | topic_name = st.selectbox("Sujet", unique_topic_names, index=None)
55 | filename = st.selectbox("Nom du Fichier", unique_filenames, index=None)
56 | document_type = st.selectbox("Type de Document", [e.value for e in DocumentType], index=None)
57 | chunk_type = st.selectbox("Type de Morceau", [e.value for e in ChunkType], index=None)
58 | #document_type = st.selectbox("Document Type", unique_document_types, index=None)
59 |
60 | filters = []
61 | if filename:
62 | filters.append({Metadata.FILENAME.value: filename})
63 | if document_type:
64 | filters.append({Metadata.DOCUMENT_TYPE.value: document_type})
65 | if topic_name:
66 | filters.append({Metadata.TOPIC.value: topic_name})
67 | if document_type:
68 | filters.append({Metadata.DOCUMENT_TYPE.value: document_type})
69 | if chunk_type:
70 | filters.append({Metadata.CHUNK_TYPE.value: chunk_type})
71 | if st.form_submit_button("Recherche"):
72 | # add check for empty string as it is not supported by bedrock (or anthropic?)
73 | if search != "":
74 | if len(filters) > 1:
75 | where = {"$and": filters}
76 | elif len(filters) == 1:
77 | where = filters[0]
78 | else:
79 | where = {}
80 | store = get_store()
81 | if isinstance(store, OpenSearchVectorSearch):
82 | result_filters = []
83 | for os_filter in filters:
84 | for key in os_filter.keys():
85 | result_filters.append({"match": {f"metadata.{key}": os_filter[key]}})
86 | result = store.similarity_search(search, k=5, boolean_filter=result_filters)
87 | else:
88 | result = store.similarity_search(search, k=5, filter=where)
89 | st.write(result)
90 | else:
91 | st.write("Veuillez entrer un texte.")
92 |
93 | st.subheader("Administration des Données")
94 |
95 | col1, col2 = st.columns(2)
96 | with col1:
97 | file_name_to_delete = st.selectbox("Choisir un fichier", unique_filenames, index=None)
98 | if st.button("Supprimer les données du fichier"):
99 | delete_documents_by_type_and_name(collection_name=collection_name, type=Metadata.FILENAME.value, name=file_name_to_delete)
100 | delete_file(file_name_to_delete, CollectionType.DOCUMENTS.value)
101 |
102 | chunk_type_to_delete = st.selectbox("Choisir un type de morceau (chunk)", [e.value for e in ChunkType], index=None)
103 | if st.button("Supprimer les données de ce type"):
104 | delete_documents_by_type_and_name(collection_name=collection_name, type=Metadata.CHUNK_TYPE.value,
105 | name=chunk_type_to_delete)
106 |
107 | with col2:
108 | topic_name_to_delete = st.selectbox("Choisir un sujet", unique_topic_names, index=None)
109 | if st.button("Supprimer les données de ce sujet"):
110 | delete_documents_by_type_and_name(collection_name=collection_name, type=Metadata.TOPIC.value, name=topic_name_to_delete)
111 |
112 | if st.button("Supprimer la collection"):
113 | empty_store(collection_name=collection_name)
114 |
115 | with st.expander("Voir toutes les meta-données", expanded=False):
116 | st.subheader("Méta-données")
117 | metadatas = get_metadatas(collection_name=collection_name)
118 | st.code(json.dumps(metadatas, indent=4, sort_keys=True), language="json")
119 |
120 |
121 | if __name__ == "__main__":
122 | if not check_password():
123 | # Do not continue if check_password is not True.
124 | st.stop()
125 | main()
126 |
--------------------------------------------------------------------------------
/tests/utils/test_utilsfile.py:
--------------------------------------------------------------------------------
1 | """Test the utilsfile file."""
2 |
3 | import unittest
4 | import os
5 | #from pytest import fixture
6 | from unittest.mock import patch
7 | from rag_assistant.utils.utilsfile import list_files, _list_files_locally, _list_files_from_s3
8 |
9 |
10 | class TestListFiles(unittest.TestCase):
11 | """Test the list_files function."""
12 |
13 | @patch('rag_assistant.utils.utilsfile.config.get')
14 | @patch('rag_assistant.utils.utilsfile._list_files_locally')
15 | def test_list_files_locally(self, mock_list_files_locally, mock_config_get):
16 | """Test list_files with LOCAL configuration."""
17 | mock_config_get.return_value = 'LOCAL'
18 | mock_list_files_locally.return_value = ['file1.txt', 'file2.txt']
19 | result = list_files('my_collection')
20 | self.assertEqual(result, ['file1.txt', 'file2.txt'])
21 | mock_list_files_locally.assert_called_once_with(file_collection='my_collection')
22 |
23 | @patch('rag_assistant.utils.utilsfile.config.get')
24 | @patch('rag_assistant.utils.utilsfile._list_files_from_s3')
25 | def test_list_files_s3(self, mock_list_files_from_s3, mock_config_get):
26 | """Test list_files with S3 configuration."""
27 | mock_config_get.return_value = 'S3'
28 | mock_list_files_from_s3.return_value = ['file1.txt', 'file2.txt']
29 | result = list_files('my_collection')
30 | self.assertEqual(result, ['file1.txt', 'file2.txt'])
31 | mock_list_files_from_s3.assert_called_once_with(file_collection='my_collection')
32 |
33 | @patch('rag_assistant.utils.utilsfile.config.get')
34 | def test_list_files_none(self, mock_config_get):
35 | """Test list_files with NONE configuration."""
36 | mock_config_get.return_value = 'NONE'
37 | result = list_files('my_collection')
38 | self.assertIsNone(result)
39 |
40 | @patch('rag_assistant.utils.utilsfile.config.get')
41 | def test_list_files_not_implemented(self, mock_config_get):
42 | """Test list_files with an unknown configuration."""
43 | mock_config_get.return_value = 'UNKNOWN'
44 | with self.assertRaises(NotImplementedError):
45 | list_files('my_collection')
46 |
47 | @patch('rag_assistant.utils.utilsfile.config.get')
48 | @patch('os.path.exists')
49 | @patch('os.listdir')
50 | def test__list_files_locally(self, mock_listdir, mock_path_exists, mock_config_get):
51 | """Test _list_files_locally function."""
52 | mock_listdir.return_value = ['file1.txt', 'file2.txt', 'file3.jpg']
53 | mock_path_exists.return_value = True
54 | mock_config_get.return_value = 'data'
55 | result = _list_files_locally('my_local_collection')
56 | self.assertEqual(result, ['file1.txt', 'file2.txt', 'file3.jpg'])
57 | mock_listdir.assert_called_once_with(os.path.join('data', 'my_local_collection'))
58 | mock_path_exists.assert_called_once_with(os.path.join('data', 'my_local_collection'))
59 |
60 | @patch('rag_assistant.utils.utilsfile.config.get')
61 | @patch('os.path.exists')
62 | @patch('os.listdir')
63 | def test__list_files_locally_empty(self, mock_listdir, mock_path_exists, mock_config_get):
64 | """Test _list_files_locally function with an empty directory."""
65 | mock_listdir.return_value = []
66 | mock_path_exists.return_value = True
67 | mock_config_get.return_value = 'data'
68 | result = _list_files_locally('empty_collection')
69 | self.assertEqual(result, [])
70 | mock_listdir.assert_called_once_with(os.path.join('data', 'empty_collection'))
71 |
72 | @patch('rag_assistant.utils.utilsfile.boto3.client')
73 | @patch('rag_assistant.utils.utilsfile.config.get')
74 | def test__list_files_from_s3(self, mock_config_get, mock_boto3_client):
75 | """Test _list_files_from_s3 function."""
76 | mock_config_get.return_value = 'my_test_bucket'
77 | mock_s3_client = mock_boto3_client.return_value
78 | mock_s3_client.list_objects_v2.return_value = {
79 | 'Contents': [
80 | {'Key': 'file_collection/file1.txt'},
81 | {'Key': 'file_collection/file2.txt'}
82 | ]
83 | }
84 | result = _list_files_from_s3('file_collection')
85 | self.assertEqual(result, ['file1.txt', 'file2.txt'])
86 | mock_boto3_client.assert_called_once_with('s3')
87 | mock_s3_client.list_objects_v2.assert_called_once_with(Bucket='my_test_bucket',
88 | Prefix='file_collection')
89 |
90 | @patch('rag_assistant.utils.utilsfile.boto3.client')
91 | @patch('rag_assistant.utils.utilsfile.config.get')
92 | def test__list_files_from_s3_empty(self, mock_config_get, mock_boto3_client):
93 | """Test _list_files_from_s3 function with an empty collection."""
94 | mock_config_get.return_value = 'my_test_bucket'
95 | mock_s3_client = mock_boto3_client.return_value
96 | mock_s3_client.list_objects_v2.return_value = {}
97 | result = _list_files_from_s3('empty_collection')
98 | self.assertEqual(result, [])
99 | mock_boto3_client.assert_called_once_with('s3')
100 | mock_s3_client.list_objects_v2.assert_called_once_with(Bucket='my_test_bucket',
101 | Prefix='empty_collection')
102 |
103 | @patch('rag_assistant.utils.utilsfile.boto3.client')
104 | @patch('rag_assistant.utils.utilsfile.config.get')
105 | def test__list_files_from_s3_exception(self, mock_config_get, mock_boto3_client):
106 | """Test _list_files_from_s3 function with an exception."""
107 | mock_config_get.return_value = 'my_test_bucket'
108 | mock_s3_client = mock_boto3_client.return_value
109 | mock_s3_client.list_objects_v2.side_effect = Exception('S3 access error')
110 | with self.assertRaises(Exception) as context:
111 | _list_files_from_s3('invalid_collection')
112 | self.assertTrue('S3 access error' in str(context.exception))
113 |
--------------------------------------------------------------------------------
/rag_assistant/utils/utilsfile.py:
--------------------------------------------------------------------------------
1 | """Utils for storing files"""
2 | import io
3 | import os
4 | import logging
5 | import boto3
6 |
7 | from streamlit.runtime.uploaded_file_manager import UploadedFile
8 |
9 | from .config_loader import load_config
10 | from .constants import StorageType
11 |
12 | config = load_config()
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | def put_file(file: io.BytesIO, filename:str, file_collection: str = '') -> None:
17 | """Persist file to selected storage interface"""
18 | storage_interface = config.get('DOCUMENTS_STORAGE', 'INTERFACE')
19 |
20 | if storage_interface == StorageType.LOCAL.value:
21 | _persist_file_locally(file, filename=filename, file_collection=file_collection)
22 | elif storage_interface == StorageType.S3.value:
23 | _persist_file_to_s3(file, filename=filename, file_collection=file_collection)
24 | elif storage_interface == StorageType.NONE.value:
25 | pass
26 | else:
27 | raise NotImplementedError(f"{storage_interface} not implemented yet for storage.")
28 |
29 |
30 | def get_file(filename: str, file_collection: str=''):
31 | """Get file from selected storage interface"""
32 | storage_interface = config.get('DOCUMENTS_STORAGE', 'INTERFACE')
33 |
34 | if storage_interface == StorageType.LOCAL.value:
35 | return _get_file_locally(filename=filename, file_collection=file_collection)
36 |
37 | if storage_interface == StorageType.S3.value:
38 | return _get_file_from_s3(filename=filename, file_collection=file_collection)
39 |
40 | if storage_interface == StorageType.NONE.value:
41 | return None
42 |
43 | raise NotImplementedError(f"{storage_interface} not implemented yet for storage.")
44 |
45 |
46 | def delete_file(filename: str, file_collection: str=''):
47 | """Delete file from selected storage interface"""
48 | storage_interface = config.get('DOCUMENTS_STORAGE', 'INTERFACE')
49 |
50 | if storage_interface == StorageType.LOCAL.value:
51 | return _delete_file_locally(filename=filename, file_collection=file_collection)
52 |
53 | if storage_interface == StorageType.S3.value:
54 | return _delete_file_from_s3(filename=filename, file_collection=file_collection)
55 |
56 | if storage_interface == StorageType.NONE.value:
57 | return None
58 |
59 | raise NotImplementedError(f"{storage_interface} not implemented yet for storage.")
60 |
61 |
62 | def list_files(file_collection: str=''):
63 | """List files from selected storage interface"""
64 | storage_interface = config.get('DOCUMENTS_STORAGE', 'INTERFACE')
65 |
66 | if storage_interface == StorageType.LOCAL.value:
67 | return _list_files_locally(file_collection=file_collection)
68 |
69 | if storage_interface == StorageType.S3.value:
70 | return _list_files_from_s3(file_collection=file_collection)
71 |
72 | if storage_interface == StorageType.NONE.value:
73 | return None
74 |
75 | raise NotImplementedError(f"{storage_interface} not implemented yet for storage.")
76 |
77 |
78 | def _persist_file_to_s3(file: io.BytesIO, filename: str, file_collection: str) -> None:
79 | """Persist file to S3 storage"""
80 | logger.info("On persiste un document : %s sur S3", filename)
81 | s3_bucket = config.get('DOCUMENTS_STORAGE', 'S3_BUCKET_NAME')
82 |
83 | file_key = f"{file_collection}/{filename}"
84 |
85 | s3_client = boto3.client('s3')
86 |
87 | s3_client.upload_fileobj(file, s3_bucket, file_key)
88 |
89 |
90 | def _persist_file_locally(file: io.BytesIO, filename:str, file_collection: str) -> None:
91 | """Persist file to local storage"""
92 | logger.info("On persiste un document : %s localement", filename)
93 | documents_path = config.get('DOCUMENTS_STORAGE', 'DOCUMENTS_PATH')
94 |
95 | file_path = os.path.join(documents_path, file_collection)
96 |
97 | if not os.path.exists(file_path):
98 | os.makedirs(file_path)
99 |
100 | file_path = os.path.join(file_path, filename)
101 |
102 | with open(file_path, 'wb') as f:
103 | f.write(file.getbuffer())
104 |
105 |
106 | def _get_file_locally(filename: str, file_collection: str):
107 | """Get file from local storage"""
108 | documents_path = config.get('DOCUMENTS_STORAGE', 'DOCUMENTS_PATH')
109 |
110 | file_path = os.path.join(documents_path, file_collection, filename)
111 |
112 | if os.path.exists(file_path):
113 | return open(file_path, 'rb').read()
114 |
115 | return None
116 |
117 |
118 | def _get_file_from_s3(filename: str, file_collection: str):
119 | """Get file from S3 storage"""
120 | s3_bucket = config.get('DOCUMENTS_STORAGE', 'S3_BUCKET_NAME')
121 |
122 | file_key = f"{file_collection}/{filename}"
123 |
124 | s3_client = boto3.client('s3')
125 |
126 | response = s3_client.get_object(Bucket=s3_bucket, Key=file_key)
127 | return response['Body'].read()
128 |
129 |
130 | def _delete_file_locally(filename: str, file_collection: str):
131 | """Delete file from local storage"""
132 | documents_path = config.get('DOCUMENTS_STORAGE', 'DOCUMENTS_PATH')
133 |
134 | file_path = os.path.join(documents_path, file_collection, filename)
135 |
136 | if os.path.exists(file_path):
137 | os.remove(file_path)
138 |
139 |
140 | def _delete_file_from_s3(filename: str, file_collection: str):
141 | """Delete file from S3 storage"""
142 | s3_bucket = config.get('DOCUMENTS_STORAGE', 'S3_BUCKET_NAME')
143 |
144 | file_key = f"{file_collection}/{filename}"
145 |
146 | s3_client = boto3.client('s3')
147 |
148 | s3_client.delete_object(Bucket=s3_bucket, Key=file_key)
149 |
150 |
151 | def _list_files_locally(file_collection: str):
152 | """List files from local storage"""
153 | documents_path = config.get('DOCUMENTS_STORAGE', 'DOCUMENTS_PATH')
154 |
155 | file_path = os.path.join(documents_path, file_collection)
156 |
157 | if os.path.exists(file_path):
158 | return os.listdir(file_path)
159 |
160 | return []
161 |
162 |
163 | def _list_files_from_s3(file_collection: str):
164 | """List files from S3 storage"""
165 | s3_bucket = config.get('DOCUMENTS_STORAGE', 'S3_BUCKET_NAME')
166 |
167 | s3_client = boto3.client('s3')
168 |
169 | response = s3_client.list_objects_v2(Bucket=s3_bucket, Prefix=file_collection)
170 |
171 | if 'Contents' in response:
172 | return [obj['Key'].split('/')[-1] for obj in response['Contents']]
173 |
174 | return []
175 |
--------------------------------------------------------------------------------
/rag_assistant/utils/utilsvision.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import hashlib
3 | import imghdr
4 | import json
5 | import os
6 | import io
7 | import shutil
8 | from typing import Optional, Union
9 |
10 | import boto3
11 | from langchain_core.documents import Document
12 | from pypdf import PdfReader
13 | from streamlit.runtime.uploaded_file_manager import UploadedFile
14 |
15 | from utils.constants import ChunkType, Metadata, CollectionType
16 | from utils.config_loader import load_config
17 | from utils.utilsdoc import clean_text
18 | from utils.utilsfile import put_file
19 |
20 | config = load_config()
21 |
22 | aws_profile_name = os.getenv("profile_name")
23 | bedrock_region_name = config["BEDROCK"]["AWS_REGION_NAME"]
24 | #bedrock_embeddings_model = config["BEDROCK"]["EMBEDDINGS_MODEL"]
25 | bedrock_endpoint_url = config["BEDROCK"]["BEDROCK_ENDPOINT_URL"]
26 | vision_model = config["VISION"]["VISION_MODEL"]
27 |
28 | boto3.setup_default_session(profile_name=os.getenv("profile_name"))
29 | bedrock = boto3.client("bedrock-runtime", bedrock_region_name, endpoint_url=bedrock_endpoint_url)
30 |
31 |
32 |
33 | extract_image_output_dir = config['VISION']['IMAGE_OUTPUT_DIR']
34 |
35 | def image_to_text(encoded_image, media_type) -> Optional[str]:
36 | system_prompt = """Describe every detail you can about this image,
37 | be extremely thorough and detail even the most minute aspects of the image.
38 | Start your description by providing an image title followed by a short overall summary.
39 | If the image is a table, output the content of the table in a structured format.
40 | """
41 |
42 | prompt = {
43 | "anthropic_version": "bedrock-2023-05-31",
44 | "max_tokens": 1000,
45 | "temperature": 0,
46 | "system": system_prompt,
47 | "messages": [
48 | {
49 | "role": "user",
50 | "content": [
51 | {
52 | "type": "image",
53 | "source": {
54 | "type": "base64",
55 | "data": encoded_image,
56 | "media_type": media_type
57 | }
58 | },
59 | {
60 | "type": "text",
61 | "text": system_prompt
62 | }
63 | ]
64 | }
65 | ]
66 | }
67 |
68 | json_prompt = json.dumps(prompt)
69 | try:
70 | response = bedrock.invoke_model(body=json_prompt, modelId=vision_model,
71 | accept="application/json", contentType="application/json")
72 | response_body = json.loads(response.get('body').read())
73 | output = response_body['content'][0]['text']
74 | return output
75 |
76 | # Catch all other (unexpected) exceptions
77 | except Exception as e:
78 | print(f"An unexpected error occurred: {e}")
79 | return None
80 |
81 |
82 | def generate_unique_id(fname):
83 | # Generate MD5 hash of the filename
84 | hash_object = hashlib.md5(fname.name.encode())
85 | hex_dig = hash_object.hexdigest()
86 | return hex_dig
87 |
88 |
89 | def load_image(pdfs: Union[list[UploadedFile], None, UploadedFile], metadata = None, restart_image_analysis:bool = False, ) -> Optional[list[Document]]:
90 | if pdfs is not None:
91 | docs = []
92 | if metadata is None:
93 | metadata = {}
94 | metadata.update({Metadata.CHUNK_TYPE.value: ChunkType.IMAGE.value})
95 | for pdf in pdfs:
96 | if pdf.type == "application/pdf":
97 | # Generate a unique identifier for each document
98 | tmp_id_based_on_file_upload = generate_unique_id(pdf)
99 | # Construct a save directory and create it
100 | save_dir = f"{extract_image_output_dir}/{tmp_id_based_on_file_upload}"
101 | if restart_image_analysis:
102 | # Before processing is done, remove the directory and its contents
103 | shutil.rmtree(save_dir)
104 |
105 | reader = PdfReader(pdf)
106 |
107 | os.makedirs(save_dir, exist_ok=True)
108 |
109 | for i, page in enumerate(reader.pages, start=1):
110 | for image in page.images:
111 |
112 | save_path = f"{save_dir}/{image.name}"
113 | json_path = f"{save_dir}/{image.name}.json"
114 |
115 | if os.path.exists(json_path):
116 | # skip the image if it is already processed
117 | with open(json_path, "r") as file: # Open the document file
118 | doc_data = json.load(file) # Load the data from the document
119 | # Create a new Document instance using the loaded data
120 | doc = Document(page_content=doc_data['page_content'], metadata=doc_data['metadata'])
121 | docs.append(doc) # Add the document to the docs list
122 | continue
123 |
124 | with open(save_path, "wb") as fp:
125 | fp.write(image.data)
126 |
127 | # Determine image type
128 | image_type = imghdr.what(save_path)
129 | media_type = f"image/{image_type}"
130 |
131 | image_content = encode_image(save_path)
132 | image_description = image_to_text(image_content, media_type)
133 | if image_description is not None:
134 | page_metadata = {'page': i, 'filename': pdf.name, 'media_type': media_type}
135 | page_metadata.update(metadata)
136 | doc = Document(page_content=clean_text(image_description), metadata=page_metadata)
137 | docs.append(doc)
138 |
139 | with open(json_path, "w") as file:
140 | json.dump(doc.__dict__, file)
141 |
142 | else:
143 | print(f"Failed to extract text from image {image.name}.")
144 |
145 | put_file(io.BytesIO(image.data), image.name, CollectionType.IMAGES.value)
146 | return docs
147 | else:
148 | return None
149 |
150 |
151 | def encode_image(image_path):
152 | """Function to encode images"""
153 | with open(image_path, "rb") as image_file:
154 | return base64.b64encode(image_file.read()).decode('utf-8')
155 |
--------------------------------------------------------------------------------
/rag_assistant/shared/llm_facade.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from utils.config_loader import load_config
4 |
5 | config = load_config()
6 |
7 |
8 | # La génération par LLM va etre fait au moment de la création du summary index.
9 | # cela prend trop de temps ici.
10 | # l'objectif est de mutualiser la fonctionnalités conversations starters pour les deux chats
11 | # llm = load_model(streaming=True)
12 | #
13 | # context = summary_query_engine.query("Make a complete summary of knowledge available"
14 | # " on following topics {topics}.")
15 | #
16 | # ### Answer question ###
17 | # cs_system_prompt = """You are a helpful solution architect and software engineer assistant.
18 | # Your users are asking questions on specific topics.\
19 | # Suggest exactly 6 questions related to the provided context to help them find the information they need. \
20 | # Suggest only short questions without compound sentences. \
21 | # Question must be self-explanatory and topic related.
22 | # Suggest a variety of questions that cover different aspects of the context. \
23 | # Use the summary of knowledge to generate the question on topics. \
24 | # Make sure they are complete questions, and that they are related to the topics.
25 | # Output one question per line. Do not number the questions. Do not group question by topics.
26 | # DO NOT make a summary or an introduction of your result. Output ONLY the generated questions.
27 | # DO NOT output chapter per topic. Avoid blank line.
28 | # Avoid duplicate question. Generate question in French.
29 | # Questions: """
30 | #
31 | # # Examples:
32 | # # What information needs to be provided during IHM launch?
33 | # # How is the data transferred to the service call?
34 | # # What functions are involved in API Management?
35 | # # What does the Exposure function in API Management entail?
36 | #
37 | # cs_prompt = ChatPromptTemplate.from_messages(
38 | # [
39 | # ("system", cs_system_prompt),
40 | # ("human", "{topics}"
41 | # "{summary}"),
42 | # ]
43 | # )
44 | # output_parser = StrOutputParser()
45 | # model = load_model(streaming=False)
46 | #
47 | # chain = cs_prompt | model | output_parser
48 | # response = chain.invoke({"topics": topics, "summary": context})
49 |
50 | def get_conversation_starters(topics: list[str], count:int = 4):
51 |
52 | response = ""
53 |
54 | response_list = [line for line in response.split("\n") if line.strip() != '']
55 | if len(response_list) > count:
56 | response_list = random.sample(response_list, count)
57 |
58 | elif len(response_list) < count:
59 | diff = count - len(response_list)
60 |
61 | suggested_questions = suggested_questions_examples
62 |
63 | # check if 'API' is in topics
64 | if 'API' in topics:
65 | suggested_questions.extend(suggested_questions_examples_api)
66 |
67 | # check if 'IHM' is in topics
68 | if 'IHM' in topics:
69 | suggested_questions.extend(suggested_questions_examples_ihm)
70 |
71 | all_questions = list(suggested_questions)
72 |
73 | selected_questions = set(response_list)
74 |
75 | while len(selected_questions) < count:
76 | question = random.choice(suggested_questions)
77 | selected_questions.add(question)
78 |
79 | response_list = list(selected_questions)
80 | # for _ in range(min(count, len(all_questions))):
81 | # question = random.choice(all_questions)
82 | # all_questions.remove(question)
83 | # response_list.append(question)
84 |
85 | #additional_questions = random.sample(suggested_questions, diff)
86 | #response_list.extend(additional_questions)
87 |
88 | return response_list
89 |
90 |
91 | suggested_questions_examples = [
92 | "Comment sécuriser les données sensibles ?",
93 | "Comment assurer l'efficacité des performances ?",
94 | "En quoi consiste l'Analyse de risque MESARI ?",
95 | "A quoi sert le Cross Origin Resource Sharing ?",
96 | "Quels sont les principes de la Content Security Policy ?",
97 | "Comment garantir la sécurité des échanges entre applications ?",
98 | "Quelles sont les bonnes pratiques pour assurer la fiabilité des ressources Web ?",
99 | "Pourquoi suivre les spécifications associées est-il important ?"
100 | ]
101 | # API
102 | suggested_questions_examples_api = [
103 | "Quels sont les mécanismes d'authentification API ?",
104 | "Quelles sont les principales fonctionnalités du portail fournisseur ?",
105 | "Que comprend la fonction d'exposition dans la gestion des API ?",
106 | "Quelle est la différence entre SOAP et REST ?",
107 | "Que signifie l'acronyme API ?",
108 | "Quels formats de données sont couramment utilisés dans les APIs ?",
109 | "Comment tester et déboguer une API ?",
110 | "Quels sont les avantages d'utiliser une API ?",
111 | "Que signifie REST et quels en sont les principes clés ?",
112 | "Comment gérer les versions dans une API ?",
113 | "Quels outils permettent de documenter une API ?",
114 | "Comment implémenter une pagination dans une API ?",
115 | "Qu'est-ce qu'une architecture d'API ?",
116 | ]
117 | suggested_questions_examples_ihm = [
118 | # IHM
119 | "Quelles informations doivent être fournies lors du lancement de l'IHM?",
120 | "Quels sont les principes de base d'une bonne conception d'interface utilisateur ?",
121 | "Comment rendre une interface utilisateur accessible aux personnes handicapées ?",
122 | "Quels sont les différents types de composants d'interface utilisateur ?",
123 | "Comment concevoir une expérience utilisateur cohérente sur différents appareils ?",
124 | "Quels sont les avantages du design 'mobile first' ?",
125 | "Comment effectuer des tests d'utilisabilité pour une interface ?",
126 | "Que signifie 'responsive design' pour une interface web ?",
127 | "Quels frameworks facilitent le développement d'interfaces utilisateur modernes ?",
128 | "Comment optimiser les performances d'une interface utilisateur ?",
129 | "Quelle est l'importance des conventions de conception dans une interface ?",
130 | ]
131 | # AUTRES QUESTIONS API
132 | # "Comment structurer une API RESTful ?",
133 | # "Quels sont les bons usages des méthodes HTTP ?",
134 | # "Comment définir des URIs pour les ressources ?",
135 | # "Qu'est-ce que HATEOAS et comment l'implémenter ?",
136 | # "Comment paginer et filtrer des collections de ressources ?",
137 | # "Quels mécanismes utiliser pour l'authentification API ?",
138 | # "Comment gérer les versions d'une API ?",
139 | # "Quelle est la stratégie de contrôle d'accès recommandée ?",
140 | # "Comment documenter une API efficacement ?",
141 | # "Comment implémenter le throttling pour une API ?",
142 | # "Quels sont les principes de conception d'une IHM intuitive ?",
143 | # "Comment assurer la résilience d'une API ?",
144 | # "Quels sont les formats standards pour les données API ?",
145 | # "Comment surveiller la performance d'une API ?",
146 | # "Quels sont les aspects de sécurité à considérer pour une API ?",
147 | # "Comment gérer la rétrocompatibilité des API ?",
148 | # "Quels sont les avantages du caching pour une API ?",
149 | # "Comment assurer la haute disponibilité d'une API ?",
150 | # "Quels outils utiliser pour le monitoring d'une API ?",
151 | # "Comment prévenir les injections dans une API ?"
152 |
153 |
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # applied-ai-rag-assistant
2 | Assistant RAG Advanced with Streamlit, Langchain, LlamaIndex and ChromaDB
3 |
4 | Initially forked from https://github.com/langchain-ai/streamlit-agent/ `chat_with_documents.py`
5 |
6 | Apps feature LangChain 🤝 Streamlit integrations such as the
7 | [Callback integration](https://python.langchain.com/docs/modules/callbacks/integrations/streamlit) and
8 | [StreamlitChatMessageHistory](https://python.langchain.com/docs/integrations/memory/streamlit_chat_message_history).
9 |
10 | Now we have added Mistral La Plateforme, Bedrock, llamaindex and langchain agent for advanced RAG, model vision on RAG with anthropic claude.
11 |
12 | ## Setup
13 |
14 | This project uses [Poetry](https://python-poetry.org/) for dependency management.
15 |
16 | ```shell
17 | # Create Python environment
18 | $ poetry install
19 |
20 | # Install git pre-commit hooks
21 | $ poetry shell
22 | $ pre-commit install
23 | ```
24 |
25 | ### Note on package dependencies
26 | For now, we are not forcing package's version in poetry and try to upgrade as fast as we can. :)
27 | As we are using a lot of new and young "GENAI" component that have not finalized their interface,
28 | application and tests tends to break a lot and often especially they are not testing evolution with each other.
29 |
30 | Main packages are:
31 | - Langchain (LLM Orchestration and agent)
32 | - LlamaIndex (RAG)
33 | - Streamlit (UX)
34 | - TruLens (Testing)
35 | - Chroma (Vector Store)
36 | - OpenAI (LLM)
37 | - MistralAI (LLM)
38 | - boto3 (for bedrock and AWS integration)
39 |
40 | ## Running
41 |
42 | ### Environment variables
43 | The project expects some environment variables to be setup in order to run.
44 | Some are mandatory for running and some are only needed if you want to run on a specific platform.
45 |
46 | The project currently supports the following platforms: OPENAI, AZURE, MISTRAL, BEDROCK (AWS).
47 |
48 | We recommend to add the variables in a .env file within the directory path outside the project directory to avoid any accidental commit.
49 | Your home directory is fine.
50 |
51 | Here are the variables:
52 |
53 | ```shell
54 | OPENAI_API_KEY=