├── .gitignore ├── Notebooks ├── 01_acs-notebook.ipynb ├── 02_blob-storage.ipynb ├── 03_indexing_api.ipynb ├── 04_indexing_api_with_blob.ipynb ├── 05_retrieval_with_lcel.ipynb ├── requirements.txt ├── restaurant │ ├── food.txt │ └── opening_hours.txt └── script.py ├── README.md ├── application ├── .env.example ├── backend │ ├── Dockerfile │ ├── app.py │ └── wait-for-postgres.sh ├── docker-compose.yaml ├── frontend │ ├── .dockerignore │ ├── .editorconfig │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── angular.json │ ├── karma.conf.js │ ├── package.json │ ├── requirements.txt │ ├── server.py │ ├── src │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── chat-ui │ │ │ │ ├── chat-ui.component.css │ │ │ │ ├── chat-ui.component.html │ │ │ │ ├── chat-ui.component.spec.ts │ │ │ │ └── chat-ui.component.ts │ │ │ ├── chatbot-instructions │ │ │ │ ├── chatbot-instructions.component.css │ │ │ │ ├── chatbot-instructions.component.html │ │ │ │ ├── chatbot-instructions.component.spec.ts │ │ │ │ └── chatbot-instructions.component.ts │ │ │ ├── food-card │ │ │ │ ├── food-card.component.css │ │ │ │ ├── food-card.component.html │ │ │ │ ├── food-card.component.spec.ts │ │ │ │ ├── food-card.component.ts │ │ │ │ └── food.models.ts │ │ │ └── header │ │ │ │ ├── header.component.css │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.spec.ts │ │ │ │ └── header.component.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── background.png │ │ │ ├── marshmallow-pizza.png │ │ │ ├── olive-branch.png │ │ │ ├── pizza-margherita.png │ │ │ ├── pizza-quattro-formaggi.png │ │ │ ├── pizza-salami.png │ │ │ ├── robot.png │ │ │ └── user.png │ │ ├── environments │ │ │ ├── environment.development.ts │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── postgres │ ├── Dockerfile │ └── init.sql └── uploadservice │ ├── .browserslistrc │ ├── .dockerignore │ ├── .editorconfig │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── angular.json │ ├── karma.conf.js │ ├── package.json │ ├── requirements.txt │ ├── server.py │ ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ ├── assets │ │ ├── .gitkeep │ │ └── upload.png │ ├── auth.service.ts │ ├── environments │ │ ├── environment.development.ts │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── azure_functions └── eventgridfunc │ ├── .gitignore │ ├── function_app.py │ ├── host.json │ └── requirements.txt └── azure_setup ├── container_registry.sh ├── db_firewall.sh └── ip_adresses.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .python_packages 3 | **/.env 4 | **/__pycache__ 5 | myapp -------------------------------------------------------------------------------- /Notebooks/01_acs-notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "from dotenv import load_dotenv, find_dotenv\n", 11 | "from azure.core.credentials import AzureKeyCredential\n", 12 | "from azure.search.documents.indexes import SearchIndexClient\n", 13 | "from azure.search.documents.indexes.models import (\n", 14 | " ComplexField,\n", 15 | " CorsOptions,\n", 16 | " SearchIndex,\n", 17 | " SearchFieldDataType,\n", 18 | " SimpleField,\n", 19 | " SearchableField\n", 20 | ")" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "load_dotenv(find_dotenv('../application/.env'))" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "endpoint = os.environ[\"SEARCH_ENDPOINT\"]\n", 39 | "key = os.environ[\"SEARCH_API_KEY\"]\n", 40 | "\n", 41 | "print(key)\n", 42 | "\n", 43 | "client = SearchIndexClient(endpoint, AzureKeyCredential(key))" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "name = \"restaurant\"\n", 53 | "fields = [\n", 54 | " SimpleField(name=\"restaurantId\", type=SearchFieldDataType.String, key=True),\n", 55 | " SimpleField(name=\"averageCost\", type=SearchFieldDataType.Double),\n", 56 | " SearchableField(name=\"description\", type=SearchFieldDataType.String),\n", 57 | " ComplexField(name=\"address\", fields=[\n", 58 | " SimpleField(name=\"streetAddress\", type=SearchFieldDataType.String),\n", 59 | " SimpleField(name=\"city\", type=SearchFieldDataType.String),\n", 60 | " ])\n", 61 | "]" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "from azure.core.exceptions import HttpResponseError\n", 71 | "\n", 72 | "cors_options = CorsOptions(allowed_origins=[\"*\"], max_age_in_seconds=60)\n", 73 | "scoring_profiles = []\n", 74 | "\n", 75 | "index = SearchIndex(\n", 76 | " name=name,\n", 77 | " fields=fields,\n", 78 | " scoring_profiles=scoring_profiles,\n", 79 | " cors_options=cors_options)\n", 80 | "\n", 81 | "\n", 82 | "try:\n", 83 | " result = client.create_index(index)\n", 84 | " print(f\"Index '{name}' created.\")\n", 85 | "except HttpResponseError as e:\n", 86 | " print(f\"Index '{name}' already exists.\")\n" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "### Add documents to the index" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "import os\n", 103 | "from azure.core.credentials import AzureKeyCredential\n", 104 | "from azure.search.documents import SearchClient\n", 105 | "\n", 106 | "index_name = \"restaurant\"\n", 107 | "endpoint = os.environ[\"SEARCH_ENDPOINT\"]\n", 108 | "key = os.environ[\"SEARCH_API_KEY\"]\n", 109 | "\n", 110 | "documents = [\n", 111 | " {\n", 112 | " 'restaurantId': '1',\n", 113 | " 'averageCost': 50.0,\n", 114 | " 'description': 'Traditional Italian cuisine with a modern twist.',\n", 115 | " 'address': {\n", 116 | " 'streetAddress': '123 Via Roma',\n", 117 | " 'city': 'Rome',\n", 118 | " }\n", 119 | " },\n", 120 | " {\n", 121 | " 'restaurantId': '2',\n", 122 | " 'averageCost': 70.0,\n", 123 | " 'description': 'Family-friendly Italian restaurant with classic dishes.',\n", 124 | " 'address': {\n", 125 | " 'streetAddress': '456 Via Milano',\n", 126 | " 'city': 'Milan',\n", 127 | " }\n", 128 | " },\n", 129 | " {\n", 130 | " 'restaurantId': '3',\n", 131 | " 'averageCost': 35.0,\n", 132 | " 'description': 'Cozy trattoria offering regional specialties.',\n", 133 | " 'address': {\n", 134 | " 'streetAddress': '789 Via Napoli',\n", 135 | " 'city': 'Naples',\n", 136 | " }\n", 137 | " }\n", 138 | "]\n", 139 | "\n", 140 | "search_client = SearchClient(endpoint, index_name, AzureKeyCredential(key))\n", 141 | "result = search_client.upload_documents(documents=documents)\n" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "for res in result:\n", 151 | " print(f\"Upload of document with ID '{res.key}' succeeded: {res.succeeded}\")" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Now we can retrieve Documents from ACS" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "results = search_client.search(search_text=\"Family friendly?\")\n", 168 | "\n", 169 | "for result in results:\n", 170 | " print(result)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "### Using LangChain with ACS" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "import os\n", 187 | "from langchain_openai import OpenAIEmbeddings\n", 188 | "from langchain.vectorstores.azuresearch import AzureSearch" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "embeddings: OpenAIEmbeddings = OpenAIEmbeddings(deployment=\"text-embedding-ada-002\", chunk_size=1)\n", 198 | "index_name: str = \"langchain-example\"\n", 199 | "vector_store: AzureSearch = AzureSearch(\n", 200 | " azure_search_endpoint=os.environ.get(\"SEARCH_ENDPOINT\"),\n", 201 | " azure_search_key=os.environ.get(\"SEARCH_API_KEY\"),\n", 202 | " index_name=index_name,\n", 203 | " embedding_function=embeddings.embed_query,\n", 204 | ")" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "from langchain_community.document_loaders import DirectoryLoader, TextLoader\n", 214 | "\n", 215 | "loader = DirectoryLoader('./restaurant', glob=\"**/*.txt\", loader_cls=TextLoader)\n", 216 | "data = loader.load()\n", 217 | "print(len(data))" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", 227 | "text_splitter = RecursiveCharacterTextSplitter(\n", 228 | " chunk_size = 120,\n", 229 | " chunk_overlap = 20,\n", 230 | " length_function = len,\n", 231 | " is_separator_regex = False,\n", 232 | ")\n", 233 | "docs = text_splitter.split_documents(data)\n", 234 | "print(len(docs))" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "vector_store.add_documents(documents=docs)" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "\n", 253 | "docs = vector_store.similarity_search(\n", 254 | " query=\"When are the opening hours of the restaurant?\",\n", 255 | " k=3,\n", 256 | " search_type=\"similarity\",\n", 257 | ")\n", 258 | "print(docs)\n" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "from langchain.chains import RetrievalQA\n", 268 | "from langchain_openai import ChatOpenAI\n", 269 | "\n", 270 | "qa = RetrievalQA.from_chain_type(llm=ChatOpenAI(), chain_type=\"stuff\", retriever=vector_store.as_retriever())\n", 271 | "qa.run(\"When are the opening hours of the restaurant?\")" 272 | ] 273 | } 274 | ], 275 | "metadata": { 276 | "kernelspec": { 277 | "display_name": "acs", 278 | "language": "python", 279 | "name": "python3" 280 | }, 281 | "language_info": { 282 | "codemirror_mode": { 283 | "name": "ipython", 284 | "version": 3 285 | }, 286 | "file_extension": ".py", 287 | "mimetype": "text/x-python", 288 | "name": "python", 289 | "nbconvert_exporter": "python", 290 | "pygments_lexer": "ipython3", 291 | "version": "3.11.0" 292 | } 293 | }, 294 | "nbformat": 4, 295 | "nbformat_minor": 2 296 | } 297 | -------------------------------------------------------------------------------- /Notebooks/02_blob-storage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from dotenv import load_dotenv, find_dotenv\n", 10 | "\n", 11 | "load_dotenv(find_dotenv('../application/.env'))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import os\n", 21 | "from azure.storage.blob import BlobServiceClient\n", 22 | "\n", 23 | "folder_path = \"./restaurant\"\n", 24 | "\n", 25 | "conn_str=os.getenv(\"BLOB_CONN_STRING\")\n", 26 | "container_name = os.getenv(\"BLOB_CONTAINER\")\n", 27 | "\n", 28 | "\n", 29 | "blob_service_client = BlobServiceClient.from_connection_string(conn_str=conn_str)\n", 30 | "container_client = blob_service_client.get_container_client(container_name)\n", 31 | "\n", 32 | "for filename in os.listdir(folder_path):\n", 33 | " if os.path.isfile(os.path.join(folder_path, filename)):\n", 34 | " file_path = os.path.join(folder_path, filename)\n", 35 | "\n", 36 | " blob_client = blob_service_client.get_blob_client(container=container_name, blob=filename)\n", 37 | "\n", 38 | " with open(file_path, \"rb\") as data:\n", 39 | " blob_client.upload_blob(data, overwrite=True)\n" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "Now we can link ACS BlobStorage and the ACS VectorStore" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "from langchain_community.document_loaders import AzureBlobStorageContainerLoader\n", 56 | "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", 57 | "\n", 58 | "loader = AzureBlobStorageContainerLoader(conn_str=conn_str, container=container_name)\n", 59 | "data = loader.load()\n", 60 | "\n", 61 | "text_splitter = RecursiveCharacterTextSplitter(\n", 62 | " chunk_size = 200,\n", 63 | " chunk_overlap = 20,\n", 64 | " length_function = len,\n", 65 | " is_separator_regex = False,\n", 66 | ")\n", 67 | "docs = text_splitter.split_documents(data)\n", 68 | "print(len(data))\n", 69 | "print(len(docs))" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "import os\n", 79 | "from langchain_openai.embeddings import OpenAIEmbeddings\n", 80 | "from langchain.vectorstores.azuresearch import AzureSearch" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "embeddings: OpenAIEmbeddings = OpenAIEmbeddings(deployment=\"text-embedding-ada-002\", chunk_size=1)\n", 90 | "index_name: str = \"restaurant2\"\n", 91 | "vector_store: AzureSearch = AzureSearch(\n", 92 | " azure_search_endpoint=os.environ.get(\"SEARCH_ENDPOINT\"),\n", 93 | " azure_search_key=os.environ.get(\"SEARCH_API_KEY\"),\n", 94 | " index_name=index_name,\n", 95 | " embedding_function=embeddings.embed_query,\n", 96 | ")" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "result = vector_store.add_documents(documents=docs)" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "If you run that code multiple times, we would add the same documents again and again - quick solution is to delete the index and create the complete index again." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "from azure.core.credentials import AzureKeyCredential\n", 122 | "from azure.search.documents import SearchClient\n", 123 | "\n", 124 | "\n", 125 | "endpoint = os.environ[\"SEARCH_ENDPOINT\"]\n", 126 | "api_key = os.environ[\"SEARCH_API_KEY\"]\n", 127 | "\n", 128 | "credential = AzureKeyCredential(api_key)\n", 129 | "client = SearchClient(endpoint=endpoint,\n", 130 | " index_name=index_name,\n", 131 | " credential=credential)\n", 132 | "\n", 133 | "results = client.search(search_text=\"*\")\n", 134 | "documents = [result for result in results]\n", 135 | "\n", 136 | "print(len(documents))\n" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "from azure.core.credentials import AzureKeyCredential\n", 146 | "from azure.search.documents.indexes import SearchIndexClient\n", 147 | "\n", 148 | "index_client = SearchIndexClient(endpoint, AzureKeyCredential(api_key))\n", 149 | "index_client.delete_index(index_name)\n", 150 | "\n", 151 | "print(f\"Index '{index_name}' has been deleted.\")\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Now we could create the Index again - but it would be actually better to:\n", 159 | "\n", 160 | "1. Not have duplicated documents in the vectorstore\n", 161 | "2. Not to drop Indexes and recreate them everything a source document changes\n", 162 | "\n", 163 | "For this issue, the indexing API was developed. Unfortunately, the indexing API does NOT work in combination with ACS.\n", 164 | "That´s why we will continue with PGVector on Azure\n" 165 | ] 166 | } 167 | ], 168 | "metadata": { 169 | "kernelspec": { 170 | "display_name": "acs", 171 | "language": "python", 172 | "name": "python3" 173 | }, 174 | "language_info": { 175 | "codemirror_mode": { 176 | "name": "ipython", 177 | "version": 3 178 | }, 179 | "file_extension": ".py", 180 | "mimetype": "text/x-python", 181 | "name": "python", 182 | "nbconvert_exporter": "python", 183 | "pygments_lexer": "ipython3", 184 | "version": "3.11.0" 185 | } 186 | }, 187 | "nbformat": 4, 188 | "nbformat_minor": 2 189 | } 190 | -------------------------------------------------------------------------------- /Notebooks/03_indexing_api.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Indexing API" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from dotenv import load_dotenv, find_dotenv\n", 17 | "\n", 18 | "load_dotenv(find_dotenv('../application/.env'))" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "Lets add Documents and Embeddings!" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "from langchain_community.document_loaders import DirectoryLoader, TextLoader\n", 35 | "from langchain.text_splitter import CharacterTextSplitter\n", 36 | "\n", 37 | "loader = DirectoryLoader('./restaurant', glob=\"**/*.txt\", loader_cls=TextLoader)\n", 38 | "data = loader.load()\n", 39 | "text_splitter = CharacterTextSplitter(chunk_size=150, chunk_overlap=20)\n", 40 | "docs = text_splitter.split_documents(data)\n", 41 | "print(len(docs))" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "import os\n", 51 | "\n", 52 | "host = os.getenv(\"PG_VECTOR_HOST\")\n", 53 | "user = os.getenv(\"PG_VECTOR_USER\")\n", 54 | "password = os.getenv(\"PG_VECTOR_PASSWORD\")\n", 55 | "database = os.getenv(\"PGDATABASE\")\n", 56 | "COLLECTION_NAME = \"langchain_collection\"\n", 57 | "\n", 58 | "CONNECTION_STRING = f\"postgresql+psycopg2://{user}:{password}@{host}:5432/{database}\"\n", 59 | "CONNECTION_STRING" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "from langchain_openai.embeddings import OpenAIEmbeddings\n", 69 | "from langchain.vectorstores.pgvector import PGVector\n", 70 | "\n", 71 | "embeddings = OpenAIEmbeddings()\n", 72 | "\n", 73 | "vector_store = PGVector(\n", 74 | " embedding_function=embeddings,\n", 75 | " collection_name=COLLECTION_NAME,\n", 76 | " connection_string=CONNECTION_STRING,\n", 77 | ")" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "from langchain.indexes import SQLRecordManager, index" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "namespace = f\"pgvector/{COLLECTION_NAME}\"\n", 96 | "record_manager = SQLRecordManager(\n", 97 | " namespace, db_url=CONNECTION_STRING\n", 98 | ")" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "record_manager.create_schema()" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "Updat the documents to see changes (2nd run)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "index(\n", 124 | " docs,\n", 125 | " record_manager,\n", 126 | " vector_store,\n", 127 | " cleanup=None,\n", 128 | " source_id_key=\"source\",\n", 129 | ")" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "from langchain.schema import Document\n", 139 | "\n", 140 | "docs[1].page_content = \"updated\"\n", 141 | "del docs[6]\n", 142 | "docs.append(Document(page_content=\"new content\", metadata={\"source\": \"important\"}))" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "index(\n", 152 | " docs,\n", 153 | " record_manager,\n", 154 | " vector_store,\n", 155 | " cleanup=None,\n", 156 | " source_id_key=\"source\",\n", 157 | ")" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "docs[1].page_content = \"updated again\"\n", 167 | "del docs[2]\n", 168 | "del docs[3]\n", 169 | "del docs[4]\n", 170 | "docs.append(Document(page_content=\"more new content\", metadata={\"source\": \"important\"}))" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "index(\n", 180 | " docs,\n", 181 | " record_manager,\n", 182 | " vector_store,\n", 183 | " cleanup=\"incremental\",\n", 184 | " source_id_key=\"source\",\n", 185 | ")" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "index(\n", 195 | " [],\n", 196 | " record_manager,\n", 197 | " vector_store,\n", 198 | " cleanup=\"incremental\",\n", 199 | " source_id_key=\"source\",\n", 200 | ")" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "index([], record_manager, vector_store, cleanup=\"full\", source_id_key=\"source\")" 210 | ] 211 | } 212 | ], 213 | "metadata": { 214 | "kernelspec": { 215 | "display_name": "Python 3", 216 | "language": "python", 217 | "name": "python3" 218 | }, 219 | "language_info": { 220 | "codemirror_mode": { 221 | "name": "ipython", 222 | "version": 3 223 | }, 224 | "file_extension": ".py", 225 | "mimetype": "text/x-python", 226 | "name": "python", 227 | "nbconvert_exporter": "python", 228 | "pygments_lexer": "ipython3", 229 | "version": "3.11.0" 230 | } 231 | }, 232 | "nbformat": 4, 233 | "nbformat_minor": 2 234 | } 235 | -------------------------------------------------------------------------------- /Notebooks/04_indexing_api_with_blob.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from dotenv import load_dotenv, find_dotenv\n", 10 | "\n", 11 | "load_dotenv(find_dotenv('../application/.env'))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from langchain.indexes import SQLRecordManager, index\n", 21 | "import os\n", 22 | "from langchain_openai.embeddings import OpenAIEmbeddings\n", 23 | "from langchain.vectorstores.pgvector import PGVector\n", 24 | "\n", 25 | "\n", 26 | "host = os.getenv(\"PG_VECTOR_HOST\")\n", 27 | "user = os.getenv(\"PG_VECTOR_USER\")\n", 28 | "password = os.getenv(\"PG_VECTOR_PASSWORD\")\n", 29 | "COLLECTION_NAME = os.getenv(\"PGDATABASE\")\n", 30 | "CONNECTION_STRING = f\"postgresql+psycopg2://{user}:{password}@{host}:5432/{COLLECTION_NAME}\"\n", 31 | "\n", 32 | "namespace = f\"pgvector/{COLLECTION_NAME}\"\n", 33 | "record_manager = SQLRecordManager(\n", 34 | " namespace, db_url=CONNECTION_STRING\n", 35 | ")\n", 36 | "\n", 37 | "embeddings = OpenAIEmbeddings()\n", 38 | "\n", 39 | "vector_store = PGVector(\n", 40 | " embedding_function=embeddings,\n", 41 | " collection_name=COLLECTION_NAME,\n", 42 | " connection_string=CONNECTION_STRING,\n", 43 | ")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "index([], record_manager, vector_store, cleanup=\"full\", source_id_key=\"source\")" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "import os\n", 62 | "from langchain_community.document_loaders import AzureBlobStorageContainerLoader\n", 63 | "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", 64 | "\n", 65 | "def load_and_split_documents(chunk_size=200, chunk_overlap=20):\n", 66 | " conn_str = os.getenv(\"BLOB_CONN_STRING\")\n", 67 | " container_name = os.getenv(\"BLOB_CONTAINER\")\n", 68 | "\n", 69 | " if conn_str is None or container_name is None:\n", 70 | " raise ValueError(\"Environment variables for BLOB_CONN_STRING or BLOB_CONTAINER are not set.\")\n", 71 | "\n", 72 | " loader = AzureBlobStorageContainerLoader(conn_str=conn_str, container=container_name)\n", 73 | " data = loader.load()\n", 74 | " for doc in data:\n", 75 | " doc.metadata[\"source\"] = os.path.basename(doc.metadata[\"source\"])\n", 76 | "\n", 77 | " text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap, length_function=len, is_separator_regex=False)\n", 78 | " return text_splitter.split_documents(data)\n" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "docs = load_and_split_documents()\n", 88 | "docs" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "index(docs, record_manager, vector_store, cleanup=\"full\", source_id_key=\"source\")" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "Lets now update something in the raw data and upload it again" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "import os\n", 114 | "from azure.storage.blob import BlobServiceClient\n", 115 | "\n", 116 | "folder_path = \"./restaurant\"\n", 117 | "\n", 118 | "conn_str=os.getenv(\"BLOB_CONN_STRING\")\n", 119 | "container_name = os.getenv(\"BLOB_CONTAINER\")\n", 120 | "\n", 121 | "\n", 122 | "blob_service_client = BlobServiceClient.from_connection_string(conn_str=conn_str)\n", 123 | "\n", 124 | "for filename in os.listdir(folder_path):\n", 125 | " if os.path.isfile(os.path.join(folder_path, filename)):\n", 126 | " file_path = os.path.join(folder_path, filename)\n", 127 | "\n", 128 | " blob_client = blob_service_client.get_blob_client(container=container_name, blob=filename)\n", 129 | "\n", 130 | " with open(file_path, \"rb\") as data:\n", 131 | " blob_client.upload_blob(data, overwrite=True)" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "docs = load_and_split_documents()\n", 141 | "for doc in docs[0:3]:\n", 142 | " print(doc)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "index(docs, record_manager, vector_store, cleanup=\"full\", source_id_key=\"source\")" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "We now have linked Blob Storage And PgVector and keep the raw docs and the indexed documents in sync. But in a real app we want to handle that automatically. Thats was Azure functions can do for us. We will take a look at Azure Functions later in the course" 159 | ] 160 | } 161 | ], 162 | "metadata": { 163 | "kernelspec": { 164 | "display_name": "acs", 165 | "language": "python", 166 | "name": "python3" 167 | }, 168 | "language_info": { 169 | "codemirror_mode": { 170 | "name": "ipython", 171 | "version": 3 172 | }, 173 | "file_extension": ".py", 174 | "mimetype": "text/x-python", 175 | "name": "python", 176 | "nbconvert_exporter": "python", 177 | "pygments_lexer": "ipython3", 178 | "version": "3.11.0" 179 | } 180 | }, 181 | "nbformat": 4, 182 | "nbformat_minor": 2 183 | } 184 | -------------------------------------------------------------------------------- /Notebooks/05_retrieval_with_lcel.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 18, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": [ 11 | "True" 12 | ] 13 | }, 14 | "execution_count": 18, 15 | "metadata": {}, 16 | "output_type": "execute_result" 17 | } 18 | ], 19 | "source": [ 20 | "from dotenv import load_dotenv, find_dotenv\n", 21 | "\n", 22 | "load_dotenv(find_dotenv('../application/.env'))" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 19, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import os\n", 32 | "from langchain_openai import ChatOpenAI\n", 33 | "from langchain.prompts import PromptTemplate\n", 34 | "from operator import itemgetter\n", 35 | "from langchain.embeddings.openai import OpenAIEmbeddings\n", 36 | "from langchain.vectorstores.pgvector import PGVector\n", 37 | "from langchain.schema.messages import get_buffer_string\n", 38 | "from langchain.schema import StrOutputParser\n", 39 | "from langchain.schema.runnable import RunnablePassthrough\n", 40 | "from langchain.schema import format_document\n", 41 | "from langchain.schema.runnable import RunnableParallel\n", 42 | "from langchain.prompts import ChatPromptTemplate\n", 43 | "\n", 44 | "host = os.getenv(\"PG_VECTOR_HOST\")\n", 45 | "user = os.getenv(\"PG_VECTOR_USER\")\n", 46 | "password = os.getenv(\"PG_VECTOR_PASSWORD\")\n", 47 | "COLLECTION_NAME = os.getenv(\"PGDATABASE\")\n", 48 | "CONNECTION_STRING = f\"postgresql+psycopg2://{user}:{password}@{host}:5432/{COLLECTION_NAME}\"\n", 49 | "\n", 50 | "embeddings = OpenAIEmbeddings(api_key=os.getenv(\"OPENAI_API_KEY\"))\n", 51 | "store = PGVector(\n", 52 | " collection_name=COLLECTION_NAME,\n", 53 | " connection_string=CONNECTION_STRING,\n", 54 | " embedding_function=embeddings,\n", 55 | ")\n", 56 | "retriever = store.as_retriever()\n", 57 | "\n", 58 | "model = ChatOpenAI()" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 20, 64 | "metadata": {}, 65 | "outputs": [ 66 | { 67 | "data": { 68 | "text/plain": [ 69 | "[Document(page_content='Monday to Thursday: 11:00 AM - 11:00 PM Friday: 11:00 AM - 12:00 AM (midnight) Saturday: 10:00 AM - 12:00 AM (midnight) Sunday: 10:00 AM - 11:00 PM Special Hours: Our kitchen closes 30 minutes before', metadata={'source': 'opening_hours.txt'}),\n", 70 | " Document(page_content='the exact amount before confirming your order. Restaurant Opening Hours:', metadata={'source': 'opening_hours.txt'}),\n", 71 | " Document(page_content='La Tavola Calda - Delivery Service & Opening Hours', metadata={'source': 'opening_hours.txt'}),\n", 72 | " Document(page_content=\"30 minutes before the restaurant closing time. Whether you're craving a quick lunch, planning a cozy dinner at home, or simply indulging in a late-night snack, La Tavola Calda is just a chat away.\", metadata={'source': 'opening_hours.txt'})]" 73 | ] 74 | }, 75 | "execution_count": 20, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "retriever.get_relevant_documents(\"When are the opening hours?\")" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 21, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "_template = \"\"\"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.\n", 91 | "\n", 92 | "Chat History:\n", 93 | "{chat_history}\n", 94 | "Follow Up Input: {question}\n", 95 | "Standalone question:\"\"\"\n", 96 | "CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(_template)\n", 97 | "\n", 98 | "template = \"\"\"Answer the question based only on the following context:\n", 99 | "{context}\n", 100 | "\n", 101 | "Question: {question}\n", 102 | "\"\"\"\n", 103 | "ANSWER_PROMPT = ChatPromptTemplate.from_template(template)\n", 104 | "DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template=\"{page_content}\")\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 22, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "def _combine_documents(\n", 114 | " docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator=\"\\n\\n\"\n", 115 | "):\n", 116 | " doc_strings = [format_document(doc, document_prompt) for doc in docs]\n", 117 | " return document_separator.join(doc_strings)" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 23, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "_inputs = RunnableParallel(\n", 127 | " standalone_question=RunnablePassthrough.assign(\n", 128 | " chat_history=lambda x: get_buffer_string(x[\"chat_history\"])\n", 129 | " )\n", 130 | " | CONDENSE_QUESTION_PROMPT\n", 131 | " | ChatOpenAI(temperature=0)\n", 132 | " | StrOutputParser(),\n", 133 | ")" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 24, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "_context = {\n", 143 | " \"context\": itemgetter(\"standalone_question\") | retriever | _combine_documents,\n", 144 | " \"question\": lambda x: x[\"standalone_question\"],\n", 145 | "}" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 25, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | ChatOpenAI() | StrOutputParser()" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 26, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "name": "stdout", 164 | "output_type": "stream", 165 | "text": [ 166 | "Intermediate Output: {'standalone_question': 'What are the opening hours?'}\n" 167 | ] 168 | }, 169 | { 170 | "data": { 171 | "text/plain": [ 172 | "\"The restaurant's opening hours are as follows:\\nMonday to Thursday: 11:00 AM - 11:00 PM\\nFriday: 11:00 AM - 12:00 AM (midnight)\\nSaturday: 10:00 AM - 12:00 AM (midnight)\\nSunday: 10:00 AM - 11:00 PM\"" 173 | ] 174 | }, 175 | "execution_count": 26, 176 | "metadata": {}, 177 | "output_type": "execute_result" 178 | } 179 | ], 180 | "source": [ 181 | "conversational_qa_chain.invoke(\n", 182 | " {\n", 183 | " \"question\": \"When are the opening hours?\",\n", 184 | " \"chat_history\": [],\n", 185 | " }\n", 186 | ")" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [] 195 | } 196 | ], 197 | "metadata": { 198 | "kernelspec": { 199 | "display_name": "app", 200 | "language": "python", 201 | "name": "python3" 202 | }, 203 | "language_info": { 204 | "codemirror_mode": { 205 | "name": "ipython", 206 | "version": 3 207 | }, 208 | "file_extension": ".py", 209 | "mimetype": "text/x-python", 210 | "name": "python", 211 | "nbconvert_exporter": "python", 212 | "pygments_lexer": "ipython3", 213 | "version": "3.11.0" 214 | } 215 | }, 216 | "nbformat": 4, 217 | "nbformat_minor": 2 218 | } 219 | -------------------------------------------------------------------------------- /Notebooks/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-search-documents==11.4.0 2 | tiktoken==0.5.2 3 | azure-identity==1.15.0 4 | azure-storage-blob==12.18.3 5 | psycopg2-binary==2.9.9 6 | unstructured==0.11.6 7 | python-dotenv==1.0.0 8 | langchain==0.1.16 9 | langchain_community==0.0.34 10 | openai==1.23.2 11 | pgvector==0.2.4 12 | langchain_openai==0.1.3 -------------------------------------------------------------------------------- /Notebooks/restaurant/food.txt: -------------------------------------------------------------------------------- 1 | At La Tavola Calda, we believe in the art of simplicity and the beauty of tradition. Our carefully curated pizza menu, featuring just four exquisite varieties, is a testament to our commitment to quality over quantity. Each pizza represents a distinct aspect of Italian culinary heritage, crafted with the freshest ingredients and the utmost care. 2 | 3 | We offer four pizzas: Margherita, Salami, Quattro Formaggi, Marshmallow 4 | 5 | Pizza Margherita - The Classic Perfection: Paying homage to the colors of the Italian flag, our Margherita is a blend of ripe tomatoes, fresh mozzarella, and basil. It's a timeless classic that lets the purity and freshness of the ingredients shine through. 6 | 7 | Pizza Salami - The Flavorful Journey: For those who crave a bit of zest, our Salami pizza offers a spicy kick. The robust flavors of cured Italian salami are balanced by the mellow mozzarella, making every bite a bold experience. 8 | 9 | Pizza Quattro Formaggi - The Cheese Lover’s Dream: A quartet of Italian cheeses, each lending its unique texture and taste, comes together for a pizza that’s sumptuously indulgent. It’s a melody of flavors that cheese aficionados will savor down to the last slice. 10 | 11 | Marshmallow Pizza - The Sweet Innovation: Embracing both tradition and creativity, this dessert pizza is a delightful surprise. It blends the warmth of toasted marshmallows with a rich chocolate sauce, creating a dessert that's unforgettably indulgent. 12 | 13 | By focusing on just four pizzas, we dedicate our time to perfect each one. We invest in sourcing the highest quality ingredients and spend hours perfecting the dough and sauce. This allows us to ensure that every pizza served is not just a meal but an authentic Italian experience. At La Tavola Calda, each pizza is not just cooked; it's crafted with passion and served with pride. -------------------------------------------------------------------------------- /Notebooks/restaurant/opening_hours.txt: -------------------------------------------------------------------------------- 1 | La Tavola Calda - Delivery Service & Opening Hours 2 | 3 | At La Tavola Calda, we bring the taste of Italy straight to your doorstep. Our modern approach to traditional dining includes a seamless delivery service designed for your convenience. By simply engaging with our friendly chatbot, you can place your order at any time, and we'll ensure that your favorite pizzas are on their way to you, hot and fresh. 4 | 5 | Delivery Service: 6 | 7 | Available: 7 days a week 8 | Delivery Hours: 11:00 AM - 10:00 PM 9 | Order Method: Directly through our user-friendly chatbot 10 | Delivery Area: We cover a wide area around our location, please check with our chatbot to confirm if your address is within our delivery zone. 11 | Delivery Fee: A small fee applies, varying depending on the distance. The chatbot will provide you with the exact amount before confirming your order. 12 | Restaurant Opening Hours: 13 | 14 | Monday to Thursday: 11:00 AM - 11:00 PM 15 | Friday: 11:00 AM - 12:00 AM (midnight) 16 | Saturday: 10:00 AM - 12:00 AM (midnight) 17 | Sunday: 10:00 AM - 11:00 PM 18 | Special Hours: Our kitchen closes 30 minutes before the restaurant closing time. 19 | Whether you're craving a quick lunch, planning a cozy dinner at home, or simply indulging in a late-night snack, La Tavola Calda is just a chat away. Enjoy the ease of ordering through our chatbot, and let us take care of the rest. Buon appetito! 20 | 21 | -------------------------------------------------------------------------------- /Notebooks/script.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from langchain.document_loaders import DirectoryLoader, TextLoader 3 | from langchain.text_splitter import CharacterTextSplitter 4 | 5 | loader = DirectoryLoader("./restaurant", glob="**/*.txt", loader_cls=TextLoader) 6 | data = loader.load() 7 | text_splitter = CharacterTextSplitter(chunk_size=150, chunk_overlap=20) 8 | docs = text_splitter.split_documents(data) 9 | 10 | documents_to_send = [ 11 | {"page_content": doc.page_content, "metadata": doc.metadata} for doc in docs 12 | ] 13 | print(documents_to_send) 14 | 15 | url = "http://localhost:5000/index_documents" 16 | 17 | try: 18 | response = requests.post(url, json=documents_to_send) 19 | response.raise_for_status() 20 | print("Response from server:", response.json()) 21 | except requests.exceptions.RequestException as e: 22 | print("An error occurred:", e) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LangChain on Azure - Udemy Course 2 | 3 | ## Course Overview 4 | 5 | This course offers a comprehensive guide on building an event-driven AI application using LangChain, focusing on frontend, backend, and microservice architecture. It explores various Azure resources and demonstrates their integration and application in a real-world scenario. 6 | 7 | ## Key Azure Resources Utilized: 8 | 9 | - Azure Cognitive Search 10 | - Azure Database for PostgreSQL Flexible Server 11 | - Blob Storage 12 | - Container Registries 13 | - Function App 14 | - Event Grid 15 | - App Service 16 | 17 | ## Get This Course 18 | 19 | For more details and to enroll in the course, visit [Udemy](https://www.udemy.com/course/langchain-on-azure-building-scalable-llm-applications). 20 | -------------------------------------------------------------------------------- /application/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | SEARCH_API_KEY= 3 | SEARCH_ENDPOINT= 4 | BLOB_CONN_STRING= 5 | BLOB_CONTAINER= 6 | PG_VECTOR_HOST= 7 | PG_VECTOR_USER= 8 | PGPORT= 9 | PGDATABASE= 10 | PG_VECTOR_PASSWORD="" -------------------------------------------------------------------------------- /application/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11-slim-buster 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the current directory contents into the container at /app 8 | COPY . /app 9 | 10 | # Install any needed packages specified in requirements.txt 11 | RUN pip install --no-cache-dir fastapi uvicorn redis requests openai tiktoken langchain langchain_openai typing_extensions python-dotenv python-multipart psycopg2-binary azure-storage-blob pgvector 12 | 13 | # Install PostgreSQL client 14 | RUN apt-get update && apt-get install -y postgresql-client && rm -rf /var/lib/apt/lists/* 15 | 16 | # Copy wait-for-postgres.sh into the container and make it executable 17 | COPY ./wait-for-postgres.sh /wait-for-postgres.sh 18 | RUN chmod +x /wait-for-postgres.sh 19 | 20 | # Make port 5000 available to the world outside this container 21 | EXPOSE 5000 22 | 23 | # Define the script to run on container start 24 | ENTRYPOINT ["/wait-for-postgres.sh"] 25 | 26 | # Define the command to run the app using Uvicorn 27 | CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000"] 28 | -------------------------------------------------------------------------------- /application/backend/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from operator import itemgetter 4 | 5 | from azure.storage.blob import BlobServiceClient 6 | from dotenv import find_dotenv, load_dotenv 7 | from fastapi import FastAPI, File, HTTPException, UploadFile 8 | from fastapi.middleware.cors import CORSMiddleware 9 | from langchain_openai import ChatOpenAI 10 | from langchain_openai import OpenAIEmbeddings 11 | from langchain.indexes import SQLRecordManager, index 12 | from langchain.prompts import ChatPromptTemplate, PromptTemplate 13 | from langchain.schema import Document, StrOutputParser, format_document 14 | from langchain.schema.runnable import RunnableParallel, RunnablePassthrough 15 | from langchain.vectorstores.pgvector import PGVector 16 | from pydantic import BaseModel, Field 17 | from sqlalchemy import create_engine 18 | from sqlalchemy.sql import text 19 | from starlette.middleware.base import BaseHTTPMiddleware 20 | from starlette.requests import Request 21 | 22 | load_dotenv(find_dotenv()) 23 | 24 | logging.basicConfig(level=logging.INFO) 25 | logger = logging.getLogger(__name__) 26 | 27 | conn_str = os.getenv("BLOB_CONN_STRING") 28 | container_name = os.getenv("BLOB_CONTAINER") 29 | blob_service_client = BlobServiceClient.from_connection_string(conn_str=conn_str) 30 | 31 | host = os.getenv("PG_VECTOR_HOST") 32 | user = os.getenv("PG_VECTOR_USER") 33 | password = os.getenv("PG_VECTOR_PASSWORD") 34 | COLLECTION_NAME = os.getenv("PGDATABASE") 35 | CONNECTION_STRING = ( 36 | f"postgresql+psycopg2://{user}:{password}@{host}:5432/{COLLECTION_NAME}" 37 | ) 38 | 39 | namespace = f"pgvector/{COLLECTION_NAME}" 40 | record_manager = SQLRecordManager(namespace, db_url=CONNECTION_STRING) 41 | record_manager.create_schema() 42 | 43 | embeddings = OpenAIEmbeddings() 44 | 45 | vector_store = PGVector( 46 | embedding_function=embeddings, 47 | collection_name=COLLECTION_NAME, 48 | connection_string=CONNECTION_STRING, 49 | ) 50 | retriever = vector_store.as_retriever() 51 | 52 | 53 | class Message(BaseModel): 54 | role: str 55 | content: str 56 | 57 | 58 | class Conversation(BaseModel): 59 | conversation: list[Message] 60 | 61 | 62 | class DocumentIn(BaseModel): 63 | page_content: str 64 | metadata: dict = Field(default_factory=dict) 65 | 66 | 67 | def _format_chat_history(conversation: list[Message]) -> str: 68 | formatted_history = "" 69 | for message in conversation: 70 | formatted_history += f"{message.role}: {message.content}\n" 71 | return formatted_history.rstrip() 72 | 73 | 74 | llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0) 75 | 76 | condense_question_template = """Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language. 77 | Chat History: 78 | {chat_history} 79 | Follow Up Input: {question} 80 | Standalone question:""" 81 | CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(condense_question_template) 82 | 83 | answer_template = """Answer the question based only on the following context: 84 | {context} 85 | 86 | If you don´t find the answer in the context, tell the user that you are happy to help with different questions about La Tavola Calda 87 | 88 | Question: {question} 89 | """ 90 | ANSWER_PROMPT = ChatPromptTemplate.from_template(answer_template) 91 | 92 | 93 | DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template="{page_content}") 94 | 95 | 96 | def _combine_documents( 97 | docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator="\n\n" 98 | ): 99 | doc_strings = [format_document(doc, document_prompt) for doc in docs] 100 | logger.info(f"Docstrings: {doc_strings}") 101 | return document_separator.join(doc_strings) 102 | 103 | 104 | _inputs = RunnableParallel( 105 | standalone_question=RunnablePassthrough.assign( 106 | chat_history=lambda x: _format_chat_history(x["chat_history"]) 107 | ) 108 | | CONDENSE_QUESTION_PROMPT 109 | | llm 110 | | StrOutputParser(), 111 | ) 112 | 113 | _context = { 114 | "context": itemgetter("standalone_question") | retriever | _combine_documents, 115 | "question": lambda x: x["standalone_question"], 116 | } 117 | conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | llm | StrOutputParser() 118 | 119 | app = FastAPI() 120 | app.add_middleware( 121 | CORSMiddleware, 122 | allow_origins=["*"], 123 | allow_credentials=True, 124 | allow_methods=["*"], 125 | allow_headers=["*"], 126 | ) 127 | 128 | 129 | @app.get("/test") 130 | async def test(): 131 | return {"test": "works"} 132 | 133 | 134 | def get_row_count(): 135 | engine = create_engine(CONNECTION_STRING) 136 | with engine.connect() as connection: 137 | result = connection.execute(text("SELECT COUNT(*) FROM langchain_pg_embedding")) 138 | row_count = result.scalar() 139 | return row_count 140 | 141 | 142 | @app.get("/row_count") 143 | async def row_count(): 144 | try: 145 | count = get_row_count() 146 | return {"row_count": count} 147 | except Exception as e: 148 | raise HTTPException(status_code=500, detail=str(e)) 149 | 150 | 151 | @app.post("/conversation") 152 | async def ask_question(question: str, conversation: Conversation) -> dict: 153 | answer = conversational_qa_chain.invoke( 154 | {"question": question, "chat_history": conversation.conversation} 155 | ) 156 | return {"answer": answer} 157 | 158 | 159 | @app.get("/listfiles") 160 | async def list_files(page: int = 1, page_size: int = 10): 161 | container_client = blob_service_client.get_container_client(container_name) 162 | blob_list = container_client.list_blobs() 163 | files = [blob.name for blob in blob_list] 164 | total_files = len(files) 165 | start = (page - 1) * page_size 166 | end = start + page_size 167 | return { 168 | "total_files": total_files, 169 | "files": files[start:end], 170 | "page": page, 171 | "total_pages": (total_files - 1) // page_size + 1, 172 | } 173 | 174 | 175 | @app.delete("/deletefile/{filename}") 176 | async def delete_file(filename: str): 177 | container_client = blob_service_client.get_container_client(container_name) 178 | blob_client = container_client.get_blob_client(blob=filename) 179 | 180 | try: 181 | blob_client.delete_blob() 182 | return {"message": f"File {filename} deleted successfully"} 183 | except Exception as e: 184 | return {"error": str(e)} 185 | 186 | 187 | @app.post("/uploadfiles") 188 | async def upload_files(files: list[UploadFile] = File(...)): 189 | container_client = blob_service_client.get_container_client(container_name) 190 | uploaded_files = [] 191 | 192 | for file in files: 193 | blob_client = container_client.get_blob_client(blob=file.filename) 194 | 195 | contents = await file.read() 196 | blob_client.upload_blob(contents, overwrite=True) 197 | uploaded_files.append(file.filename) 198 | 199 | return {"uploaded_files": uploaded_files} 200 | 201 | 202 | @app.post("/index_documents/") 203 | async def index_documents(documents_in: list[DocumentIn]): 204 | print(documents_in) 205 | try: 206 | documents = [ 207 | Document(page_content=doc.page_content, metadata=doc.metadata) 208 | for doc in documents_in 209 | ] 210 | result = index( 211 | documents, 212 | record_manager, 213 | vector_store, 214 | cleanup="full", 215 | source_id_key="source", 216 | ) 217 | return result 218 | except Exception as e: 219 | raise HTTPException(status_code=500, detail=str(e)) 220 | -------------------------------------------------------------------------------- /application/backend/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # wait-for-postgres.sh 3 | 4 | set -e 5 | 6 | # Fetch the host and port from the environment variables 7 | host="$PG_VECTOR_HOST" 8 | port="$PGPORT" 9 | user="$PG_VECTOR_USER" 10 | dbname="$PGDATABASE" 11 | password="$PG_VECTOR_PASSWORD" 12 | 13 | # Wait for PostgreSQL to become available 14 | until PGPASSWORD=$password psql -h "$host" -U "$user" -d "$dbname" -p "$port" -c '\q'; do 15 | >&2 echo "Postgres is unavailable - sleeping" 16 | sleep 1 17 | done 18 | 19 | >&2 echo "Postgres is up - executing command" 20 | 21 | # Now we can execute the CMD passed to the Docker container 22 | exec "$@" 23 | -------------------------------------------------------------------------------- /application/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | build: ./postgres 5 | ports: 6 | - "5432:5432" 7 | volumes: 8 | - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql 9 | environment: 10 | POSTGRES_USER: admin 11 | POSTGRES_PASSWORD: admin 12 | POSTGRES_DB: pgvector 13 | 14 | backend: 15 | build: ./backend 16 | ports: 17 | - "5000:5000" 18 | depends_on: 19 | - postgres 20 | env_file: 21 | - ./.env 22 | 23 | frontend: 24 | build: ./frontend 25 | ports: 26 | - "3000:3000" 27 | 28 | uploadservice: 29 | build: ./uploadservice 30 | ports: 31 | - "4000:4000" 32 | -------------------------------------------------------------------------------- /application/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /application/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /application/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /application/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Base stage with Node.js 18 for building frontend assets 2 | FROM node:18-buster-slim as build-stage 3 | 4 | WORKDIR /app 5 | 6 | # Copy package.json and yarn.lock files 7 | COPY package.json yarn.lock /app/ 8 | 9 | # Install dependencies 10 | RUN yarn install --frozen-lockfile 11 | 12 | # Copy the rest of your frontend application 13 | COPY . /app 14 | 15 | # Build the application 16 | RUN yarn build 17 | 18 | # Stage 2: Production stage for FastAPI application 19 | FROM python:3.11-slim-buster 20 | 21 | WORKDIR /app 22 | 23 | # Install Python dependencies 24 | COPY requirements.txt /app/ 25 | RUN pip install --no-cache-dir -r requirements.txt 26 | 27 | # Copy built frontend assets from build-stage 28 | COPY --from=build-stage /app/dist/frontend /app/dist/frontend 29 | 30 | # Copy only the necessary files 31 | COPY . /app 32 | 33 | EXPOSE 3000 34 | 35 | CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "3000"] 36 | -------------------------------------------------------------------------------- /application/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /application/frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "frontend": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/frontend", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": ["src/favicon.ico", "src/assets"], 29 | "styles": ["src/styles.css"], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "initial", 37 | "maximumWarning": "500kb", 38 | "maximumError": "1mb" 39 | }, 40 | { 41 | "type": "anyComponentStyle", 42 | "maximumWarning": "2kb", 43 | "maximumError": "4kb" 44 | } 45 | ], 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "outputHashing": "all" 53 | }, 54 | "development": { 55 | "buildOptimizer": false, 56 | "optimization": false, 57 | "vendorChunk": true, 58 | "extractLicenses": false, 59 | "sourceMap": true, 60 | "namedChunks": true, 61 | "fileReplacements": [ 62 | { 63 | "replace": "src/environments/environment.ts", 64 | "with": "src/environments/environment.development.ts" 65 | } 66 | ] 67 | } 68 | }, 69 | "defaultConfiguration": "production" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "configurations": { 74 | "production": { 75 | "buildTarget": "frontend:build:production" 76 | }, 77 | "development": { 78 | "buildTarget": "frontend:build:development" 79 | } 80 | }, 81 | "defaultConfiguration": "development" 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "buildTarget": "frontend:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "assets": ["src/favicon.ico", "src/assets"], 97 | "styles": ["src/styles.css"], 98 | "scripts": [] 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /application/frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/frontend'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /application/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --configuration production", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^17.0.5", 14 | "@angular/common": "^17.0.5", 15 | "@angular/compiler": "^17.0.5", 16 | "@angular/core": "^17.0.5", 17 | "@angular/forms": "^17.0.5", 18 | "@angular/platform-browser": "^17.0.5", 19 | "@angular/platform-browser-dynamic": "^17.0.5", 20 | "@angular/router": "^17.0.5", 21 | "rxjs": "~7.4.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.14.2" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^17.0.5", 27 | "@angular/cli": "^17.0.5", 28 | "@angular/compiler-cli": "^17.0.5", 29 | "@types/jasmine": "~3.10.0", 30 | "@types/node": "^12.11.1", 31 | "jasmine-core": "~3.10.0", 32 | "karma": "~6.3.0", 33 | "karma-chrome-launcher": "~3.1.0", 34 | "karma-coverage": "~2.1.0", 35 | "karma-jasmine": "~4.0.0", 36 | "karma-jasmine-html-reporter": "~1.7.0", 37 | "typescript": "~5.2.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /application/frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx -------------------------------------------------------------------------------- /application/frontend/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import httpx 5 | from fastapi import FastAPI, HTTPException, Request 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from fastapi.responses import HTMLResponse 8 | from fastapi.staticfiles import StaticFiles 9 | from starlette.middleware.base import BaseHTTPMiddleware 10 | 11 | logger = logging.getLogger("proxy_logger") 12 | logger.setLevel(logging.INFO) # Set to logging.DEBUG for more verbose output 13 | 14 | handler = logging.StreamHandler() # Writes to stderr 15 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 16 | handler.setFormatter(formatter) 17 | 18 | logger.addHandler(handler) 19 | 20 | 21 | app = FastAPI() 22 | 23 | # CORS-Middleware-Konfiguration 24 | app.add_middleware( 25 | CORSMiddleware, 26 | allow_origins=["*"], 27 | allow_credentials=True, 28 | allow_methods=["*"], 29 | allow_headers=["*"], 30 | ) 31 | 32 | 33 | class ProxyMiddleware(BaseHTTPMiddleware): 34 | async def dispatch(self, request: Request, call_next): 35 | if request.url.path.startswith("/conversation"): 36 | proxy_target = os.getenv("TARGET", "http://backend:5000") 37 | path = str(request.url).replace(str(request.base_url), "") 38 | proxy_url = f"{proxy_target.rstrip('/')}/{path.lstrip('/')}" 39 | 40 | print(f"Proxying to {proxy_url}") # Log the target proxy URL 41 | 42 | headers = { 43 | key: value for key, value in request.headers.items() if key != "host" 44 | } 45 | print(f"Request headers: {headers}") # Log the headers being forwarded 46 | 47 | try: 48 | async with httpx.AsyncClient(timeout=20) as client: 49 | response = await client.request( 50 | method=request.method, 51 | url=proxy_url, 52 | headers=headers, 53 | data=await request.body(), 54 | follow_redirects=False, 55 | ) 56 | return HTMLResponse( 57 | content=response.content, 58 | status_code=response.status_code, 59 | headers=dict(response.headers), 60 | ) 61 | except httpx.RequestError as exc: 62 | raise HTTPException( 63 | status_code=500, detail="Backend Communication Failed" 64 | ) 65 | else: 66 | return await call_next(request) 67 | 68 | 69 | app.add_middleware(ProxyMiddleware) 70 | 71 | app.mount("", StaticFiles(directory="dist/frontend", html=True), name="static") 72 | 73 | if __name__ == "__main__": 74 | port = int(os.getenv("PORT", 3000)) 75 | import uvicorn 76 | 77 | uvicorn.run(app, host="0.0.0.0", port=port) 78 | -------------------------------------------------------------------------------- /application/frontend/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .food-card-container { 2 | display: grid; 3 | grid-template-columns: repeat(4, 1fr); 4 | grid-gap: 20px; 5 | padding: 20px; 6 | justify-items: center; 7 | } 8 | 9 | @media (max-width: 900px) { 10 | .food-card-container { 11 | grid-template-columns: repeat(2, 1fr); 12 | } 13 | } 14 | 15 | @media (max-width: 688px) { 16 | .food-card-container { 17 | grid-template-columns: 1fr; 18 | } 19 | } 20 | 21 | .instructions-wrapper { 22 | display: flex; 23 | justify-content: center; 24 | margin: 4rem 0; 25 | } 26 | 27 | .chatbot-instructions-container { 28 | max-width: 600px; 29 | margin: 0 auto; 30 | } 31 | -------------------------------------------------------------------------------- /application/frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 |
9 | 10 |
-------------------------------------------------------------------------------- /application/frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Food } from './food-card/food.models'; 4 | import { FoodCardComponent } from './food-card/food-card.component'; 5 | import { ChatbotInstructionsComponent } from './chatbot-instructions/chatbot-instructions.component'; 6 | import { ChatUiComponent } from './chat-ui/chat-ui.component'; 7 | import { HeaderComponent } from './header/header.component'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | standalone: true, 12 | imports: [ 13 | CommonModule, 14 | FoodCardComponent, 15 | ChatbotInstructionsComponent, 16 | ChatUiComponent, 17 | HeaderComponent, 18 | ], 19 | templateUrl: './app.component.html', 20 | styleUrl: './app.component.css', 21 | }) 22 | export class AppComponent { 23 | foods: Food[] = [ 24 | { 25 | imageUrl: '/assets/pizza-margherita.png', 26 | name: 'Pizza Margherita', 27 | price: 10.99, 28 | }, 29 | { 30 | imageUrl: '/assets/pizza-salami.png', 31 | name: 'Pizza Salami', 32 | price: 11.99, 33 | }, 34 | { 35 | imageUrl: '/assets/pizza-quattro-formaggi.png', 36 | name: 'Pizza Quattro Formaggi', 37 | price: 13.99, 38 | }, 39 | { 40 | imageUrl: '/assets/marshmallow-pizza.png', 41 | name: 'Marshmallow Pizza', 42 | price: 14.99, 43 | }, 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /application/frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { CommonModule } from '@angular/common'; 6 | import { AppComponent } from './app.component'; 7 | 8 | @NgModule({ 9 | declarations: [], 10 | imports: [HttpClientModule, BrowserModule, FormsModule, CommonModule], 11 | providers: [], 12 | bootstrap: [AppComponent], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /application/frontend/src/app/chat-ui/chat-ui.component.css: -------------------------------------------------------------------------------- 1 | .chat-ui { 2 | position: absolute; 3 | bottom: 20px; 4 | right: 20px; 5 | transition: all 0.3s ease; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | overflow: hidden; 10 | } 11 | 12 | .chat-ui.visible { 13 | flex-direction: column; 14 | width: 450px; 15 | height: 400px; 16 | border-radius: 8px; 17 | background-color: #ffffff; 18 | border: 1px solid #ddd; 19 | } 20 | 21 | .chat-container { 22 | display: flex; 23 | flex-direction: column; 24 | height: 100%; 25 | width: 100%; 26 | } 27 | 28 | .chat-header { 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | padding: 10px; 33 | background-color: #333; 34 | color: white; 35 | } 36 | 37 | .open-chat-btn { 38 | background: none; 39 | border: none; 40 | cursor: pointer; 41 | width: 120px; 42 | height: 120px; 43 | } 44 | 45 | .open-chat-btn img { 46 | width: 100%; 47 | height: auto; 48 | border-radius: 100%; 49 | } 50 | 51 | .close-btn { 52 | margin-left: auto; 53 | background: none; 54 | border: none; 55 | cursor: pointer; 56 | color: white; 57 | } 58 | 59 | .chat-body { 60 | flex-grow: 1; 61 | padding: 15px; 62 | overflow-y: auto; 63 | } 64 | 65 | .chat-ui .message-icon { 66 | width: 30px; 67 | height: 30px; 68 | } 69 | 70 | .message { 71 | display: flex; 72 | align-items: center; 73 | margin-bottom: 10px; 74 | } 75 | 76 | .user-message { 77 | justify-content: flex-start; 78 | } 79 | 80 | .bot-message { 81 | justify-content: flex-start; 82 | } 83 | 84 | .user-message .message-text, 85 | .bot-message .message-text { 86 | background-color: #f1f1f1; 87 | border-radius: 15px; 88 | padding: 10px 20px; 89 | max-width: 80%; 90 | margin-left: 10px; 91 | } 92 | 93 | .message-icon.user, 94 | .message-icon.bot { 95 | width: 30px; 96 | height: 30px; 97 | margin-right: 10px; 98 | } 99 | 100 | .chat-input input { 101 | width: 78%; 102 | padding-left: 2%; 103 | padding-top: 8px; 104 | padding-bottom: 8px; 105 | border: 2px solid #ccc; 106 | font-size: 16px; 107 | margin-right: -4px; 108 | } 109 | 110 | .chat-input button { 111 | width: 20%; 112 | padding-top: 12px; 113 | padding-bottom: 12px; 114 | border: none; 115 | background-color: #7a2a2a; 116 | color: white; 117 | font-size: 16px; 118 | cursor: pointer; 119 | } 120 | 121 | @media (max-width: 688px) { 122 | .chat-ui.visible { 123 | width: 90%; 124 | } 125 | } 126 | 127 | .loading-indicator { 128 | display: flex; 129 | justify-content: center; 130 | align-items: center; 131 | height: 30px; 132 | } 133 | 134 | .dot { 135 | width: 8px; 136 | height: 8px; 137 | background-color: #333; 138 | border-radius: 50%; 139 | margin: 0 4px; 140 | animation: dotFlashing 1s infinite linear alternate; 141 | } 142 | 143 | .dot:nth-child(1) { 144 | animation-delay: 0s; 145 | } 146 | .dot:nth-child(2) { 147 | animation-delay: 0.2s; 148 | } 149 | .dot:nth-child(3) { 150 | animation-delay: 0.4s; 151 | } 152 | 153 | @keyframes dotFlashing { 154 | 0% { 155 | background-color: #333; 156 | } 157 | 50%, 158 | 100% { 159 | background-color: transparent; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /application/frontend/src/app/chat-ui/chat-ui.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

Chat

6 | 7 |
8 |
9 | 10 |
12 | Bot 13 | User 14 |
{{ message.text }}
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 26 | 27 |
28 |
29 | 32 |
-------------------------------------------------------------------------------- /application/frontend/src/app/chat-ui/chat-ui.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatUiComponent } from './chat-ui.component'; 4 | 5 | describe('ChatUiComponent', () => { 6 | let component: ChatUiComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChatUiComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatUiComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /application/frontend/src/app/chat-ui/chat-ui.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { environment } from '../../environments/environment'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { CommonModule } from '@angular/common'; 5 | import { HttpClient, HttpParams } from '@angular/common/http'; 6 | 7 | interface ChatMessage { 8 | text: string; 9 | sender: 'bot' | 'user'; 10 | isLoading?: boolean; // Optional property for loading state 11 | } 12 | 13 | @Component({ 14 | selector: 'app-chat-ui', 15 | templateUrl: './chat-ui.component.html', 16 | styleUrls: ['./chat-ui.component.css'], 17 | standalone: true, 18 | imports: [FormsModule, CommonModule], 19 | }) 20 | export class ChatUiComponent implements OnInit { 21 | public isVisible: boolean = false; 22 | public messages: ChatMessage[] = []; 23 | public newMessage: string = ''; 24 | 25 | constructor(private http: HttpClient) {} 26 | 27 | ngOnInit(): void {} 28 | 29 | toggleChat(): void { 30 | this.messages = []; 31 | this.newMessage = ''; 32 | this.isVisible = !this.isVisible; 33 | } 34 | 35 | sendUserMessage(): void { 36 | if (this.newMessage.trim()) { 37 | // Add user's message to the chat 38 | this.messages.push({ text: this.newMessage, sender: 'user' }); 39 | 40 | // Add a loading indicator for bot's response 41 | this.messages.push({ text: '', sender: 'bot', isLoading: true }); 42 | 43 | // Send message to API 44 | this.sendMessageToApi(this.newMessage); 45 | this.newMessage = ''; 46 | } 47 | } 48 | 49 | sendMessageToApi(newMessage: string): void { 50 | const apiUrl = `${environment.backendHost}/conversation`; 51 | const params = new HttpParams().set('question', newMessage); 52 | const payload = { 53 | conversation: this.messages.map((msg) => ({ 54 | role: msg.sender, 55 | content: msg.text, 56 | })), 57 | }; 58 | 59 | this.http.post(apiUrl, payload, { params }).subscribe((response) => { 60 | this.messages.pop(); // Remove the loading indicator 61 | this.messages.push({ text: response.answer, sender: 'bot' }); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /application/frontend/src/app/chatbot-instructions/chatbot-instructions.component.css: -------------------------------------------------------------------------------- 1 | .chatbot-instructions-container { 2 | font-family: "Roboto", sans-serif; 3 | padding: 1em; 4 | background-color: rgba(255, 255, 255, 0.9); 5 | border-radius: 5px; 6 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 7 | text-align: center; 8 | position: relative; 9 | z-index: 10; 10 | } 11 | 12 | .chatbot-instructions-container p { 13 | margin-bottom: 0.5em; 14 | color: #333; 15 | } 16 | -------------------------------------------------------------------------------- /application/frontend/src/app/chatbot-instructions/chatbot-instructions.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to La Tavola Calda - A Modern Twist on Italian Tradition!

3 |

We embrace innovation while honoring timeless traditions. Here, every dish tells a story, and every order is a 4 | conversation.

5 |

To place an order, simply interact with our friendly chatbot at any time. It's designed to ensure a seamless 6 | ordering experience, just as if you were ordering from our staff in person.

7 |

Enjoy a unique blend of technology and taste with La Tavola Calda, where convenience meets authentic Italian 8 | flavors.

9 |
-------------------------------------------------------------------------------- /application/frontend/src/app/chatbot-instructions/chatbot-instructions.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatbotInstructionsComponent } from './chatbot-instructions.component'; 4 | 5 | describe('ChatbotInstructionsComponent', () => { 6 | let component: ChatbotInstructionsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChatbotInstructionsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatbotInstructionsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /application/frontend/src/app/chatbot-instructions/chatbot-instructions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-chatbot-instructions', 5 | templateUrl: './chatbot-instructions.component.html', 6 | styleUrls: ['./chatbot-instructions.component.css'], 7 | standalone: true, 8 | }) 9 | export class ChatbotInstructionsComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit(): void {} 13 | } 14 | -------------------------------------------------------------------------------- /application/frontend/src/app/food-card/food-card.component.css: -------------------------------------------------------------------------------- 1 | .food-card { 2 | width: 100%; 3 | max-width: 400px; 4 | border: 1px solid #ddd; 5 | border-radius: 4px; 6 | overflow: hidden; 7 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 8 | transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; 9 | } 10 | 11 | .food-card:hover { 12 | transform: translateY(-5px); 13 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); 14 | } 15 | 16 | .food-image { 17 | width: 100%; 18 | height: 300px; 19 | object-fit: cover; 20 | } 21 | 22 | .food-details { 23 | padding: 15px; 24 | text-align: center; 25 | background-color: white; 26 | } 27 | 28 | .food-details h3 { 29 | margin-top: 0; 30 | color: #333; 31 | } 32 | 33 | .food-details p { 34 | margin: 5px 0; 35 | color: #666; 36 | } 37 | -------------------------------------------------------------------------------- /application/frontend/src/app/food-card/food-card.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ food.name }} 3 |
4 |

{{ food.name }}

5 |

Price: {{ food.price }}

6 |
7 |
-------------------------------------------------------------------------------- /application/frontend/src/app/food-card/food-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FoodCardComponent } from './food-card.component'; 4 | 5 | describe('FoodCardComponent', () => { 6 | let component: FoodCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FoodCardComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FoodCardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /application/frontend/src/app/food-card/food-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Food } from './food.models'; 3 | 4 | @Component({ 5 | selector: 'app-food-card', 6 | templateUrl: './food-card.component.html', 7 | styleUrls: ['./food-card.component.css'], 8 | standalone: true, 9 | }) 10 | export class FoodCardComponent { 11 | @Input() food!: Food; 12 | } 13 | -------------------------------------------------------------------------------- /application/frontend/src/app/food-card/food.models.ts: -------------------------------------------------------------------------------- 1 | export interface Food { 2 | imageUrl: string; 3 | name: string; 4 | price: number; 5 | } 6 | -------------------------------------------------------------------------------- /application/frontend/src/app/header/header.component.css: -------------------------------------------------------------------------------- 1 | .header-container { 2 | text-align: center; 3 | padding: 20px; 4 | background-color: rgba(255, 255, 255, 0.8); 5 | } 6 | 7 | .restaurant-name { 8 | font-family: "Roboto", sans-serif; 9 | font-weight: 900; 10 | font-size: 3em; 11 | color: #7a2a2a; 12 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); 13 | } 14 | 15 | .tagline { 16 | font-family: "Roboto", sans-serif; 17 | font-weight: 400; 18 | font-size: 1.5em; 19 | color: #555; 20 | text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); 21 | margin-top: 10px; 22 | } 23 | 24 | .decoration .decorative-icon { 25 | width: 70px; 26 | height: auto; 27 | margin-top: 10px; 28 | } 29 | -------------------------------------------------------------------------------- /application/frontend/src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 |

La Tavola Calda

3 |

Authentic Italian Cuisine & Timeless Traditions

4 |
5 | Olive Branch 6 |
7 |
-------------------------------------------------------------------------------- /application/frontend/src/app/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /application/frontend/src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header', 5 | templateUrl: './header.component.html', 6 | styleUrls: ['./header.component.css'], 7 | standalone: true, 8 | }) 9 | export class HeaderComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit(): void {} 13 | } 14 | -------------------------------------------------------------------------------- /application/frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /application/frontend/src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/background.png -------------------------------------------------------------------------------- /application/frontend/src/assets/marshmallow-pizza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/marshmallow-pizza.png -------------------------------------------------------------------------------- /application/frontend/src/assets/olive-branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/olive-branch.png -------------------------------------------------------------------------------- /application/frontend/src/assets/pizza-margherita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/pizza-margherita.png -------------------------------------------------------------------------------- /application/frontend/src/assets/pizza-quattro-formaggi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/pizza-quattro-formaggi.png -------------------------------------------------------------------------------- /application/frontend/src/assets/pizza-salami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/pizza-salami.png -------------------------------------------------------------------------------- /application/frontend/src/assets/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/robot.png -------------------------------------------------------------------------------- /application/frontend/src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/assets/user.png -------------------------------------------------------------------------------- /application/frontend/src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | backendHost: 'http://localhost:5000/conversation', 4 | }; 5 | -------------------------------------------------------------------------------- /application/frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // export const environment = { 2 | // production: true, 3 | // backendHost: 'https://myrestaurantapp.azurewebsites.net', 4 | // }; 5 | 6 | export const environment = { 7 | production: true, 8 | backendHost: 'http://localhost:3000', 9 | }; 10 | -------------------------------------------------------------------------------- /application/frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | backendHost: 'http://localhost:3000', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /application/frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/frontend/src/favicon.ico -------------------------------------------------------------------------------- /application/frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | La Tavola Calda 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /application/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /application/frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /application/frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap"); 2 | 3 | /* CSS Reset */ 4 | html, 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | font-size: 100%; 10 | font: inherit; 11 | vertical-align: baseline; 12 | } 13 | 14 | body { 15 | line-height: 1; 16 | } 17 | 18 | ol, 19 | ul { 20 | list-style: none; 21 | } 22 | 23 | blockquote, 24 | q { 25 | quotes: none; 26 | } 27 | 28 | blockquote:before, 29 | blockquote:after, 30 | q:before, 31 | q:after { 32 | content: ""; 33 | content: none; 34 | } 35 | 36 | table { 37 | border-collapse: collapse; 38 | border-spacing: 0; 39 | } 40 | 41 | body { 42 | font-family: "Playfair Display", serif; 43 | min-height: 100vh; 44 | width: 100%; 45 | position: relative; /* Wichtig für die Positionierung des Pseudo-Elements */ 46 | margin: 0; /* Entfernt standardmäßige Browser-Margins */ 47 | font-weight: 700; 48 | } 49 | 50 | body::before { 51 | content: ""; 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | background-image: url("/assets/background.png"); 58 | background-size: cover; /* Stellt sicher, dass der Hintergrund das gesamte Element bedeckt */ 59 | background-position: center; /* Zentriert das Bild */ 60 | background-repeat: no-repeat; /* Verhindert das Wiederholen des Bildes */ 61 | filter: grayscale(70%); /* Wendet den Graufilter an */ 62 | z-index: -1; /* Stellt sicher, dass das Bild hinter dem Inhalt liegt */ 63 | } 64 | -------------------------------------------------------------------------------- /application/frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /application/frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /application/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ], 25 | "useDefineForClassFields": false 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /application/frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /application/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:13 2 | 3 | RUN apt-get update && apt-get upgrade -y && \ 4 | apt-get install -y wget build-essential postgresql-server-dev-13 5 | 6 | RUN wget https://github.com/pgvector/pgvector/archive/v0.4.4.tar.gz && \ 7 | tar -xzvf v0.4.4.tar.gz && \ 8 | cd pgvector-0.4.4 && \ 9 | make && \ 10 | make install 11 | 12 | -------------------------------------------------------------------------------- /application/postgres/init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION vector; -------------------------------------------------------------------------------- /application/uploadservice/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /application/uploadservice/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /application/uploadservice/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /application/uploadservice/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /application/uploadservice/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Base stage with Node.js 18 for building frontend assets 2 | FROM node:18-buster-slim as build-stage 3 | 4 | WORKDIR /app 5 | 6 | # Copy package.json and yarn.lock files 7 | COPY package.json yarn.lock /app/ 8 | 9 | # Install dependencies 10 | RUN yarn install --frozen-lockfile 11 | 12 | # Copy the rest of your frontend application 13 | COPY . /app 14 | 15 | # Build the application 16 | RUN yarn build 17 | 18 | 19 | # Stage 2: Production stage for FastAPI application 20 | FROM python:3.11-slim-buster 21 | 22 | # Set working directory 23 | WORKDIR /app 24 | 25 | # Install Python dependencies 26 | COPY requirements.txt /app/ 27 | RUN pip install --no-cache-dir -r requirements.txt 28 | 29 | # Copy built frontend assets from build-stage 30 | COPY --from=build-stage /app/dist/uploadservice /app/dist/uploadservice 31 | 32 | # Copy the FastAPI application files 33 | COPY . /app 34 | 35 | # Expose the port FastAPI will run on 36 | EXPOSE 4000 37 | 38 | # Start the FastAPI application 39 | CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "4000"] 40 | -------------------------------------------------------------------------------- /application/uploadservice/README.md: -------------------------------------------------------------------------------- 1 | # Uploadservice 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /application/uploadservice/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "uploadservice": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/uploadservice", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.css" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "budgets": [ 40 | { 41 | "type": "initial", 42 | "maximumWarning": "500kb", 43 | "maximumError": "1mb" 44 | }, 45 | { 46 | "type": "anyComponentStyle", 47 | "maximumWarning": "2kb", 48 | "maximumError": "4kb" 49 | } 50 | ], 51 | "fileReplacements": [ 52 | { 53 | "replace": "src/environments/environment.ts", 54 | "with": "src/environments/environment.prod.ts" 55 | } 56 | ], 57 | "outputHashing": "all" 58 | }, 59 | "development": { 60 | "buildOptimizer": false, 61 | "optimization": false, 62 | "vendorChunk": true, 63 | "extractLicenses": false, 64 | "sourceMap": true, 65 | "namedChunks": true 66 | } 67 | }, 68 | "defaultConfiguration": "production" 69 | }, 70 | "serve": { 71 | "builder": "@angular-devkit/build-angular:dev-server", 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "uploadservice:build:production" 75 | }, 76 | "development": { 77 | "browserTarget": "uploadservice:build:development" 78 | } 79 | }, 80 | "defaultConfiguration": "development" 81 | }, 82 | "extract-i18n": { 83 | "builder": "@angular-devkit/build-angular:extract-i18n", 84 | "options": { 85 | "browserTarget": "uploadservice:build" 86 | } 87 | }, 88 | "test": { 89 | "builder": "@angular-devkit/build-angular:karma", 90 | "options": { 91 | "main": "src/test.ts", 92 | "polyfills": "src/polyfills.ts", 93 | "tsConfig": "tsconfig.spec.json", 94 | "karmaConfig": "karma.conf.js", 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ], 99 | "styles": [ 100 | "src/styles.css" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "defaultProject": "uploadservice" 109 | } 110 | -------------------------------------------------------------------------------- /application/uploadservice/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/uploadservice'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /application/uploadservice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uploadservice", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --configuration production", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "~13.1.0", 14 | "@angular/common": "~13.1.0", 15 | "@angular/compiler": "~13.1.0", 16 | "@angular/core": "~13.1.0", 17 | "@angular/forms": "~13.1.0", 18 | "@angular/platform-browser": "~13.1.0", 19 | "@angular/platform-browser-dynamic": "~13.1.0", 20 | "@angular/router": "~13.1.0", 21 | "rxjs": "~7.4.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.11.4" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "~13.1.2", 27 | "@angular/cli": "~13.1.2", 28 | "@angular/compiler-cli": "~13.1.0", 29 | "@types/jasmine": "~3.10.0", 30 | "@types/node": "^12.11.1", 31 | "jasmine-core": "~3.10.0", 32 | "karma": "~6.3.0", 33 | "karma-chrome-launcher": "~3.1.0", 34 | "karma-coverage": "~2.1.0", 35 | "karma-jasmine": "~4.0.0", 36 | "karma-jasmine-html-reporter": "~1.7.0", 37 | "typescript": "~4.5.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /application/uploadservice/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx -------------------------------------------------------------------------------- /application/uploadservice/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import httpx 5 | from fastapi import FastAPI, HTTPException, Request 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from fastapi.responses import HTMLResponse 8 | from fastapi.staticfiles import StaticFiles 9 | from starlette.middleware.base import BaseHTTPMiddleware 10 | 11 | # Logger configuration 12 | logger = logging.getLogger("proxy_logger") 13 | logger.setLevel(logging.INFO) 14 | handler = logging.StreamHandler() 15 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 16 | handler.setFormatter(formatter) 17 | logger.addHandler(handler) 18 | 19 | app = FastAPI() 20 | 21 | # CORS middleware configuration 22 | app.add_middleware( 23 | CORSMiddleware, 24 | allow_origins=["*"], 25 | allow_credentials=True, 26 | allow_methods=["*"], 27 | allow_headers=["*"], 28 | ) 29 | 30 | 31 | class ProxyMiddleware(BaseHTTPMiddleware): 32 | async def dispatch(self, request: Request, call_next): 33 | # Define a list of endpoints that need to be proxied 34 | proxied_endpoints = [ 35 | "/conversation", 36 | "/deletefile", 37 | "/listfiles", 38 | "/uploadfiles", 39 | ] 40 | 41 | if any(request.url.path.startswith(endpoint) for endpoint in proxied_endpoints): 42 | proxy_target = os.getenv("TARGET", "http://backend:5000") 43 | 44 | path = str(request.url).replace(str(request.base_url), "") 45 | proxy_url = f"{proxy_target.rstrip('/')}/{path.lstrip('/')}" 46 | 47 | print("PROXY_TARGET: ", proxy_url) 48 | 49 | logger.info(f"Proxying to {proxy_url}") 50 | 51 | headers = { 52 | key: value for key, value in request.headers.items() if key != "host" 53 | } 54 | 55 | try: 56 | async with httpx.AsyncClient(timeout=20) as client: 57 | response = await client.request( 58 | method=request.method, 59 | url=proxy_url, 60 | headers=headers, 61 | data=await request.body(), 62 | follow_redirects=False, 63 | ) 64 | return HTMLResponse( 65 | content=response.content, 66 | status_code=response.status_code, 67 | headers=dict(response.headers), 68 | ) 69 | except httpx.RequestError as exc: 70 | raise HTTPException( 71 | status_code=500, detail="Backend Communication Failed" 72 | ) 73 | else: 74 | return await call_next(request) 75 | 76 | 77 | # Add the modified middleware 78 | app.add_middleware(ProxyMiddleware) 79 | 80 | # Serve static files 81 | app.mount("", StaticFiles(directory="dist/uploadservice", html=True), name="static") 82 | 83 | if __name__ == "__main__": 84 | port = int(os.getenv("PORT", 3000)) 85 | import uvicorn 86 | 87 | uvicorn.run(app, host="0.0.0.0", port=port) 88 | -------------------------------------------------------------------------------- /application/uploadservice/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .outer-wrapper { 2 | display: block; 3 | width: 100%; 4 | margin: auto; 5 | height: 100vh; 6 | color: white; 7 | } 8 | 9 | .drop-zone { 10 | border: 2px dashed #007bff; 11 | border-radius: 5px; 12 | padding: 20px; 13 | margin-top: 10px; 14 | background-color: #f8f9fa; 15 | color: #6c757d; 16 | cursor: pointer; 17 | } 18 | 19 | .drop-zone:hover { 20 | background-color: #e2e6ea; 21 | } 22 | 23 | .drop-zone input[type="file"] { 24 | display: none; 25 | } 26 | 27 | button { 28 | margin-top: 10px; 29 | padding: 10px 15px; 30 | border: none; 31 | background-color: #007bff; 32 | color: white; 33 | border-radius: 5px; 34 | cursor: pointer; 35 | } 36 | 37 | button:hover { 38 | background-color: #0056b3; 39 | } 40 | 41 | .success { 42 | background-color: green; 43 | } 44 | 45 | .image { 46 | background-color: red; 47 | } 48 | 49 | .space { 50 | color: white; 51 | padding: 8px 0; 52 | margin-bottom: 20px; 53 | font-size: 32px; 54 | } 55 | 56 | .upload-wrapper { 57 | width: 80%; 58 | max-width: 700px; 59 | margin: 0 auto; 60 | margin-bottom: 20px; 61 | padding-top: 30px; 62 | text-align: center; 63 | } 64 | 65 | .file-list-container { 66 | width: 80%; 67 | max-width: 700px; 68 | margin: 0 auto; 69 | background-color: rgba(24, 24, 24, 0.7); 70 | border: 1px solid white; 71 | border-radius: 5px; 72 | } 73 | 74 | .file-list-table { 75 | width: 100%; 76 | border-collapse: collapse; 77 | border: 1px solid #ffffff; 78 | } 79 | 80 | .file-list-table thead { 81 | font-weight: bold; 82 | text-align: left; 83 | border-bottom: 2px solid #ffffff; 84 | } 85 | 86 | .file-list-table td, 87 | .file-list-table th { 88 | padding: 10px; 89 | border-bottom: 1px solid #ffffff; 90 | } 91 | 92 | .delete-btn { 93 | background-color: red; 94 | color: white; 95 | border: none; 96 | padding: 5px 10px; 97 | cursor: pointer; 98 | } 99 | 100 | .delete-btn:hover { 101 | background-color: darkred; 102 | } 103 | 104 | .pagination-nav { 105 | text-align: center; 106 | padding: 10px; 107 | cursor: pointer; 108 | } 109 | 110 | .pagination { 111 | display: inline-block; 112 | padding-left: 0; 113 | border-radius: 0.25rem; 114 | } 115 | 116 | .page-item { 117 | display: inline; 118 | } 119 | 120 | .page-link { 121 | color: #ff0073; 122 | padding: 0.5rem 0.75rem; 123 | } 124 | 125 | .page-item.active .page-link { 126 | color: white; 127 | background-color: #ff0073; 128 | border-color: #ff0055; 129 | } 130 | 131 | .page-item.disabled .page-link { 132 | color: #6c757d; 133 | pointer-events: none; 134 | background-color: white; 135 | border-color: #dee2e6; 136 | } 137 | -------------------------------------------------------------------------------- /application/uploadservice/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

File Upload

5 |
6 | Drag files here or click to select files 7 | 8 | 9 |
10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
FilenameActions
{{ file }}
28 | 29 | 30 | 43 |
44 |
-------------------------------------------------------------------------------- /application/uploadservice/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'uploadservice'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('uploadservice'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('uploadservice app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /application/uploadservice/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { environment } from '../environments/environment'; 5 | import { ChangeDetectorRef } from '@angular/core'; 6 | import { AuthService } from '../auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.css'], 12 | }) 13 | export class AppComponent { 14 | uploadedFiles: string[] = []; 15 | files: File[] = []; 16 | isImage: boolean = false; 17 | currentPage: number = 1; 18 | pageSize: number = 10; 19 | totalPages: number = 0; 20 | displayedFiles: string[] = []; 21 | pages: number[] = []; 22 | 23 | constructor( 24 | private authService: AuthService, 25 | private http: HttpClient, 26 | private cdr: ChangeDetectorRef 27 | ) {} 28 | 29 | ngOnInit() { 30 | console.log('Application initialized'); 31 | if (!this.authService.checkAuthentication()) { 32 | console.log(`environment: ${environment.backendHost}`); 33 | this.promptForCredentials(); 34 | } else { 35 | this.fetchUploadedFiles(); 36 | } 37 | } 38 | 39 | promptForCredentials(): void { 40 | const username = window.prompt('Username:'); 41 | const password = window.prompt('Password:'); 42 | 43 | if (username === null || password === null) { 44 | console.log('Authentication cancelled'); 45 | return; 46 | } 47 | 48 | if (!this.authService.login(username, password)) { 49 | console.log('Wrong credentials'); 50 | } else { 51 | this.fetchUploadedFiles(); 52 | } 53 | } 54 | 55 | changePage(page: number): void { 56 | if (page < 1 || page > this.totalPages) return; 57 | this.currentPage = page; 58 | this.fetchUploadedFiles(); 59 | } 60 | 61 | onFileSelect(event: Event): void { 62 | const input = event.target as HTMLInputElement; 63 | if (!input.files) return; 64 | this.files = Array.from(input.files); 65 | this.isImage = this.files.some((file) => file.type.startsWith('image/')); 66 | } 67 | 68 | onDragOver(event: Event): void { 69 | event.preventDefault(); 70 | } 71 | 72 | onDragLeave(event: Event): void { 73 | // Logic for DragLeave-Event 74 | } 75 | 76 | onDrop(event: DragEvent): void { 77 | event.preventDefault(); 78 | if (event.dataTransfer && event.dataTransfer.files) { 79 | this.files = Array.from(event.dataTransfer.files); 80 | this.isImage = this.files.some((file) => file.type.startsWith('image/')); 81 | } 82 | } 83 | 84 | containsImageFile(): boolean { 85 | return this.files.some((file) => file.type.startsWith('image/')); 86 | } 87 | 88 | async deleteFile(fileName: string): Promise { 89 | const apiUrl = `${environment.backendHost}/deletefile/${fileName}`; 90 | console.log(`Attempting to delete file: ${fileName}`); 91 | try { 92 | await firstValueFrom(this.http.delete(apiUrl)); 93 | console.log(`File deleted: ${fileName}`); 94 | this.uploadedFiles = this.uploadedFiles.filter( 95 | (file) => file !== fileName 96 | ); 97 | this.displayedFiles = this.displayedFiles.filter( 98 | (file) => file !== fileName 99 | ); 100 | this.cdr.detectChanges(); 101 | } catch (error) { 102 | console.error('Fehler beim Löschen', error); 103 | } 104 | } 105 | 106 | async fetchUploadedFiles(): Promise { 107 | const apiUrl = `${environment.backendHost}/listfiles?page=${this.currentPage}&size=${this.pageSize}`; 108 | console.log(`Fetching uploaded files from: ${apiUrl}`); 109 | try { 110 | const response = await firstValueFrom(this.http.get(apiUrl)); 111 | console.log('Response:', response); 112 | 113 | if (response.files && response.files.length > 0) { 114 | this.uploadedFiles = response.files; 115 | this.displayedFiles = response.files; 116 | this.totalPages = response.total_pages; 117 | this.pages = Array.from({ length: this.totalPages }, (_, i) => i + 1); 118 | console.log(`Files fetched: ${this.uploadedFiles}`); 119 | } else { 120 | console.log('No files returned from the server.'); 121 | } 122 | } catch (error) { 123 | console.error('Fehler beim Abrufen der Dateiliste', error); 124 | } 125 | } 126 | 127 | async uploadFiles(): Promise { 128 | const formData = new FormData(); 129 | for (const file of this.files) { 130 | formData.append('files', file, file.name); 131 | } 132 | 133 | const apiUrl = `${environment.backendHost}/uploadfiles`; 134 | console.log(`Uploading files to: ${apiUrl}`); 135 | 136 | try { 137 | await firstValueFrom(this.http.post(apiUrl, formData)); 138 | console.log('Files uploaded successfully'); 139 | await this.fetchUploadedFiles(); 140 | this.files = []; 141 | this.cdr.detectChanges(); 142 | } catch (error) { 143 | console.error('Fehler beim Hochladen', error); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /application/uploadservice/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [BrowserModule, HttpClientModule], 10 | providers: [], 11 | bootstrap: [AppComponent], 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /application/uploadservice/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/uploadservice/src/assets/.gitkeep -------------------------------------------------------------------------------- /application/uploadservice/src/assets/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/uploadservice/src/assets/upload.png -------------------------------------------------------------------------------- /application/uploadservice/src/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class AuthService { 7 | private isAuthenticated: boolean = false; 8 | 9 | constructor() {} 10 | 11 | login(username: string, password: string): boolean { 12 | this.isAuthenticated = username === 'admin' && password === 'admin'; 13 | return this.isAuthenticated; 14 | } 15 | 16 | checkAuthentication(): boolean { 17 | return this.isAuthenticated; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /application/uploadservice/src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | backendHost: 'http://localhost:5000/conversation', 4 | }; 5 | -------------------------------------------------------------------------------- /application/uploadservice/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // export const environment = { 2 | // production: true, 3 | // backendHost: 'https://codingudemybackend.azurewebsites.net', 4 | // }; 5 | 6 | export const environment = { 7 | production: true, 8 | backendHost: 'http://localhost:4000', 9 | }; 10 | -------------------------------------------------------------------------------- /application/uploadservice/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | backendHost: 'http://localhost:4000', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /application/uploadservice/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Crashkurse/LangChain-on-Azure-Udemy/1abe8bb38600e38828af72fb2f0934b5223f6a11/application/uploadservice/src/favicon.ico -------------------------------------------------------------------------------- /application/uploadservice/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Uploadservice 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /application/uploadservice/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /application/uploadservice/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /application/uploadservice/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap"); 2 | 3 | /* CSS Reset */ 4 | html, 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | font-size: 100%; 10 | font: inherit; 11 | vertical-align: baseline; 12 | } 13 | 14 | body { 15 | line-height: 1; 16 | } 17 | 18 | ol, 19 | ul { 20 | list-style: none; 21 | } 22 | 23 | blockquote, 24 | q { 25 | quotes: none; 26 | } 27 | 28 | blockquote:before, 29 | blockquote:after, 30 | q:before, 31 | q:after { 32 | content: ""; 33 | content: none; 34 | } 35 | 36 | table { 37 | border-collapse: collapse; 38 | border-spacing: 0; 39 | } 40 | 41 | body { 42 | font-family: "Playfair Display", serif; 43 | min-height: 100vh; 44 | width: 100%; 45 | position: relative; /* Wichtig für die Positionierung des Pseudo-Elements */ 46 | margin: 0; /* Entfernt standardmäßige Browser-Margins */ 47 | font-weight: 700; 48 | } 49 | 50 | body::before { 51 | content: ""; 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | background-image: url("/assets/upload.png"); 58 | background-size: cover; /* Stellt sicher, dass der Hintergrund das gesamte Element bedeckt */ 59 | background-position: center; /* Zentriert das Bild */ 60 | background-repeat: no-repeat; /* Verhindert das Wiederholen des Bildes */ 61 | filter: grayscale(70%); /* Wendet den Graufilter an */ 62 | z-index: -1; /* Stellt sicher, dass das Bild hinter dem Inhalt liegt */ 63 | } 64 | -------------------------------------------------------------------------------- /application/uploadservice/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /application/uploadservice/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /application/uploadservice/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /application/uploadservice/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /azure_functions/eventgridfunc/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | # Azurite artifacts 46 | __blobstorage__ 47 | __queuestorage__ 48 | __azurite_db*__.json -------------------------------------------------------------------------------- /azure_functions/eventgridfunc/function_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import requests 4 | import azure.functions as func 5 | from azure.storage.blob import BlobServiceClient 6 | from langchain_community.document_loaders import AzureBlobStorageContainerLoader 7 | from langchain.text_splitter import RecursiveCharacterTextSplitter 8 | 9 | app = func.FunctionApp() 10 | 11 | FASTAPI_ENDPOINT = os.getenv("FASTAPI_INDEX_DOCUMENTS_ENDPOINT") 12 | BLOB_CONN_STRING = os.getenv("BLOB_CONN_STRING") 13 | CONTAINER_NAME = os.getenv("BLOB_CONTAINER") 14 | 15 | CHUNK_SIZE = 250 16 | CHUNK_OVERLAP = 20 17 | 18 | if not FASTAPI_ENDPOINT: 19 | raise ValueError( 20 | "FASTAPI_INDEX_DOCUMENTS_ENDPOINT environment variable is not set." 21 | ) 22 | 23 | blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONN_STRING) 24 | 25 | 26 | @app.function_name(name="myblobtrigger") 27 | @app.event_grid_trigger(arg_name="event") 28 | def eventGridTest(event: func.EventGridEvent): 29 | loader = AzureBlobStorageContainerLoader( 30 | conn_str=BLOB_CONN_STRING, container=CONTAINER_NAME 31 | ) 32 | data = loader.load() 33 | 34 | text_splitter = RecursiveCharacterTextSplitter( 35 | chunk_size=CHUNK_SIZE, 36 | chunk_overlap=CHUNK_OVERLAP, 37 | length_function=len, 38 | is_separator_regex=False, 39 | ) 40 | split_documents = text_splitter.split_documents(data) 41 | 42 | logging.info(f"Document count: {len(split_documents)}") 43 | documents_in = [ 44 | { 45 | "page_content": doc.page_content, 46 | "metadata": {"source": doc.metadata["source"]}, 47 | } 48 | for doc in split_documents 49 | ] 50 | response = requests.post(FASTAPI_ENDPOINT, json=documents_in) 51 | 52 | if response.status_code == 200: 53 | logging.info("Documents sent successfully to FastAPI endpoint.") 54 | else: 55 | logging.error( 56 | f"Failed to send documents. Status Code: {response.status_code} Response: {response.text}" 57 | ) 58 | -------------------------------------------------------------------------------- /azure_functions/eventgridfunc/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /azure_functions/eventgridfunc/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | requests 3 | azure-storage-blob 4 | # Add the specific version of langchain if available, or just the package name if the latest version is needed 5 | langchain 6 | unstructured -------------------------------------------------------------------------------- /azure_setup/container_registry.sh: -------------------------------------------------------------------------------- 1 | docker build -t udemyserviceregistry.azurecr.io/backend:latest ./backend 2 | docker build -t udemyserviceregistry.azurecr.io/frontend:latest ./frontend 3 | docker build -t udemyserviceregistry.azurecr.io/uploadservice:latest ./uploadservice 4 | 5 | az acr login --name udemyserviceregistry 6 | 7 | docker push udemyserviceregistry.azurecr.io/backend:latest 8 | docker push udemyserviceregistry.azurecr.io/frontend:latest 9 | docker push udemyserviceregistry.azurecr.io/uploadservice:latest 10 | 11 | az acr repository list --name udemyserviceregistry --output table 12 | -------------------------------------------------------------------------------- /azure_setup/db_firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sourceResourceGroup="" 4 | sourceAppName="" 5 | 6 | targetResourceGroup="" 7 | targetServerName="" 8 | 9 | outboundIPs=$(az webapp show --resource-group $sourceResourceGroup --name $sourceAppName --query outboundIpAddresses --output tsv) 10 | additionalOutboundIPs=$(az webapp show --resource-group $sourceResourceGroup --name $sourceAppName --query possibleOutboundIpAddresses --output tsv) 11 | 12 | combinedIPs=$(echo "$outboundIPs,$additionalOutboundIPs" | tr ',' '\n' | sort -u) 13 | 14 | for ip in $combinedIPs; do 15 | az postgres flexible-server firewall-rule create --resource-group $targetResourceGroup --name $targetServerName --rule-name "Allow$(echo $ip | tr '.' '_')" --start-ip-address $ip --end-ip-address $ip 16 | done 17 | -------------------------------------------------------------------------------- /azure_setup/ip_adresses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sourceResourceGroup="" 4 | sourceAppName="" 5 | 6 | targetResourceGroup="" 7 | targetAppName="" 8 | priority=100 9 | 10 | outboundIPs=$(az webapp show --resource-group $sourceResourceGroup --name $sourceAppName --query outboundIpAddresses --output tsv) 11 | 12 | additionalOutboundIPs=$(az webapp show --resource-group $sourceResourceGroup --name $sourceAppName --query possibleOutboundIpAddresses --output tsv) 13 | 14 | combinedIPs=$(echo "$outboundIPs,$additionalOutboundIPs" | tr ',' '\n' | sort -u) 15 | 16 | for ip in $combinedIPs; do 17 | az webapp config access-restriction add --resource-group $targetResourceGroup --name $targetAppName --rule-name "Allow_$ip" --action Allow --ip-address $ip/32 --priority $priority 18 | done 19 | 20 | az webapp config access-restriction add --resource-group $targetResourceGroup --name $targetAppName --rule-name "DenyAll" --action Deny --ip-address "0.0.0.0/0" --priority 500 21 | --------------------------------------------------------------------------------