├── src ├── __init__.py ├── validators │ ├── __init__.py │ ├── pinecone_validators.py │ └── agent_validators.py ├── vector_database │ ├── __init__.py │ ├── main.py │ └── utils.py ├── utils.py ├── agent.py └── agent_tools.py ├── langgraph.json ├── logging_config.py ├── requirements.txt ├── data ├── syntetic_data │ ├── studies_status.csv │ ├── get_availability.py │ └── patients_info.csv └── catalog.json ├── README.md ├── .gitignore └── faq └── data.json /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/validators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vector_database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "python_version": "3.11", 3 | "dockerfile_lines": [], 4 | "dependencies": [ 5 | "." 6 | ], 7 | "graphs": { 8 | "agent": "./src/agent.py:app" 9 | }, 10 | "env": ".env" 11 | } -------------------------------------------------------------------------------- /logging_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig( 4 | level=logging.INFO, 5 | format='%(asctime)s - %(message)s', 6 | handlers=[ 7 | logging.FileHandler("app.log"), 8 | logging.StreamHandler() 9 | ] 10 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langgraph 2 | langchain 3 | langchain_openai 4 | langchain_community 5 | langchain_core 6 | python-dotenv 7 | langchain_pinecone 8 | pinecone 9 | jq 10 | langchain-google-genai 11 | pandas 12 | langchain_anthropic 13 | langchain_groq -------------------------------------------------------------------------------- /src/vector_database/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR=os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | from src.vector_database.utils import PineconeManagment 11 | 12 | def deploy_vectordatabase(index_name): 13 | vdb_app = PineconeManagment() 14 | docs = vdb_app.reading_datasource() 15 | vdb_app.creating_index(index_name = index_name, docs = docs) 16 | 17 | 18 | 19 | if __name__ == '__main__': 20 | deploy_vectordatabase(index_name = 'ovidedentalclinic') -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | from langchain_openai import ChatOpenAI 2 | from langchain_groq import ChatGroq 3 | from langchain_anthropic import ChatAnthropic 4 | from langchain_google_genai.chat_models import ChatGoogleGenerativeAI 5 | from typing import Literal 6 | 7 | def format_retrieved_docs(docs): 8 | return "\n\n".join(doc.page_content for doc in docs) 9 | 10 | def get_model(provider:Literal['openai','google','meta','anthropic']): 11 | if provider == "openai": 12 | return ChatOpenAI(temperature=0, model_name="gpt-4o-mini", strict = True) 13 | elif provider == "anthropic": 14 | return ChatAnthropic(temperature=0, model_name="claude-3-5-sonnet") 15 | elif provider == "google": 16 | return ChatGoogleGenerativeAI(temperature=0, model_name="gemini-1.5-pro-exp-0801") 17 | elif provider == "meta": 18 | return ChatGroq(temperature=0, model_name="llama-3.1-70b-versatile") 19 | -------------------------------------------------------------------------------- /src/validators/pinecone_validators.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR=os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | from pydantic import BaseModel, field_validator, Field 11 | import re 12 | from typing import Dict 13 | 14 | class IndexNameStructure(BaseModel): 15 | index_name: str = Field(description="Lower case name of the index you want to create") 16 | 17 | 18 | @field_validator('index_name') 19 | def check_letters_lowercase(cls, v): 20 | if not re.fullmatch(r"^[a-z]+$", v): 21 | raise ValueError('index_name must be only letters in lowercase') 22 | return v 23 | 24 | class ExpectedNewData(BaseModel): 25 | new_info: Dict[str, str] = Field(description="Expected a pair key:value of question and answer.") 26 | 27 | 28 | @field_validator('new_info') 29 | def check_lowercase(cls, v): 30 | if set(v.keys()) != {'question','answer'}: 31 | raise ValueError("The structure of the dictionary should be {'question':'...,' 'answer':'...'}") 32 | return v -------------------------------------------------------------------------------- /data/syntetic_data/studies_status.csv: -------------------------------------------------------------------------------- 1 | patient_id,medical_study,is_available 2 | 1000094,Plaque Index,False 3 | 1000009,Cephalometric X-Ray,True 4 | 1000079,CBCT Scan,True 5 | 1000095,Periodontal Probing,False 6 | 1000079,Cephalometric X-Ray,False 7 | 1000026,Dental X-Ray,True 8 | 1000052,Gingival Index,False 9 | 1000055,Panoramic X-Ray,True 10 | 1000032,Panoramic X-Ray,True 11 | 1000096,Cephalometric X-Ray,False 12 | 1000089,Occlusal Analysis,False 13 | 1000041,Oral Cancer Screening,True 14 | 1000044,CBCT Scan,True 15 | 1000061,Oral Cancer Screening,True 16 | 1000086,Dental X-Ray,True 17 | 1000048,Gingival Index,False 18 | 1000095,TMJ Examination,False 19 | 1000024,Panoramic X-Ray,True 20 | 1000082,Gingival Index,False 21 | 1000000,Caries Risk Assessment,False 22 | 1000006,CBCT Scan,True 23 | 1000090,Caries Risk Assessment,False 24 | 1000048,Cephalometric X-Ray,True 25 | 1000016,Cephalometric X-Ray,False 26 | 1000037,Gingival Index,False 27 | 1000012,TMJ Examination,False 28 | 1000037,Dental X-Ray,False 29 | 1000089,CBCT Scan,True 30 | 1000035,Gingival Index,True 31 | 1000063,Bitewing X-Ray,True 32 | 1000094,Occlusal Analysis,True 33 | 1000047,Panoramic X-Ray,True 34 | 1000002,Periodontal Probing,True 35 | 1000046,Plaque Index,False 36 | 1000010,Bitewing X-Ray,True 37 | 1000060,Plaque Index,True 38 | 1000010,Periodontal Probing,True 39 | 1000055,TMJ Examination,True 40 | 1000004,Cephalometric X-Ray,False 41 | 1000072,Panoramic X-Ray,True 42 | -------------------------------------------------------------------------------- /src/validators/agent_validators.py: -------------------------------------------------------------------------------- 1 | from pydantic import constr, BaseModel, Field, validator 2 | import re 3 | 4 | 5 | class DateTimeModel(BaseModel): 6 | """ 7 | The way the date should be structured and formatted 8 | """ 9 | date: str = Field(..., description="Propertly formatted date", pattern=r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$') 10 | 11 | @validator("date") 12 | def check_format_date(cls, v): 13 | if not re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$', v): 14 | raise ValueError("The date should be in format 'YYYY-MM-DD HH:MM'") 15 | return v 16 | class DateModel(BaseModel): 17 | """ 18 | The way the date should be structured and formatted 19 | """ 20 | date: str = Field(..., description="Propertly formatted date", pattern=r'^\d{4}-\d{2}-\d{2}$') 21 | 22 | @validator("date") 23 | def check_format_date(cls, v): 24 | if not re.match(r'^\d{4}-\d{2}-\d{2}$', v): 25 | raise ValueError("The date must be in the format 'YYYY-MM-DD'") 26 | return v 27 | 28 | 29 | class IdentificationNumberModel(BaseModel): 30 | """ 31 | The way the ID should be structured and formatted 32 | """ 33 | id: int = Field(..., description="identification number without dots", pattern=r'^\d{7,8}$') 34 | 35 | @validator("id") 36 | def check_format_id(cls, v): 37 | if not re.match(r'^\d{7,8}$',str(v)): 38 | raise ValueError("The ID number should be a number of 7 or 8 numbers") 39 | return v 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # agentic-customer-service-medical-dental-clinic 2 | This software contains an agent based on LangGraph & LangChain for solving general client queries and it could be implemented in whatever channel of this medical clinic (Whatsapp, Telegram, Instagram, etc). 3 | 4 | # AI Autonomous Agent 5 | It is highly autonomous and it is able to handle different types of secretary tasks like: give general information about the clinic, cancel, reschedule and set appointments, check doctor availability, review if your results are ready, which services the dental clinic offers, etc. 6 | 7 | 8 | # Workflow 9 | 10 | ![image](https://github.com/user-attachments/assets/bcdac740-5a3a-42aa-8d8e-c8410a2bf675) 11 | 12 | 13 | ## Some use cases: 14 | 15 | 16 | This is a casual chat where the agent books and reschedule books easily: 17 | 18 | https://github.com/user-attachments/assets/cd4d9983-c2c3-4844-9bcb-4314c6137bbf 19 | 20 | Here I provided a wrong ID number and we can see how it handle errors quite good. Also, I provided a demo of how it handle general questions with a RAG approach. 21 | 22 | https://github.com/user-attachments/assets/d36e5a0b-0d49-4cb8-a7e4-7d68ef206c9c 23 | 24 | 25 | # How to use 26 | 27 | 1) Set the following ENV variables 28 | 29 | WORKDIR (The root path to the repository) 30 | XXX_API_KEY (LLM provider you want to use) 31 | PINECONE_API_KEY 32 | 33 | 2) If it is your first time, execute the main.py file inside the vector_database directory. This will create the Vector Database. 34 | 35 | 3) Execute the get_availability.py file inside the syntetic_data directory in order to have data up to date 36 | 37 | 4) Run the app executing agent.py / Run the app using LangGraph Studio -------------------------------------------------------------------------------- /src/vector_database/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR=os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | from langchain_community.document_loaders import JSONLoader 11 | from pinecone import Pinecone, ServerlessSpec 12 | from dotenv import load_dotenv 13 | import time 14 | from langchain_pinecone import PineconeVectorStore 15 | from langchain_openai import OpenAIEmbeddings 16 | from langchain_core.documents import Document 17 | from typing import Dict 18 | from src.validators.pinecone_validators import IndexNameStructure, ExpectedNewData 19 | import logging 20 | import logging_config 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class PineconeManagment: 26 | 27 | def __init__(self): 28 | logger.info("Setting pinecone connection...") 29 | 30 | def __extract_metadata(self, record: dict, metadata: dict) -> dict: 31 | 32 | metadata["question"] = record['question'] 33 | logger.info("Metadata extracted!") 34 | return metadata 35 | 36 | def reading_datasource(self): 37 | loader = JSONLoader( 38 | file_path=f'{WORKDIR}/faq/data.json', 39 | jq_schema='.[]', 40 | text_content=False, 41 | metadata_func=self.__extract_metadata) 42 | 43 | return loader.load() 44 | 45 | def creating_index(self, index_name: str, docs: Document, dimension=1536, metric="cosine", embedding = OpenAIEmbeddings(model="text-embedding-ada-002")): 46 | logger.info(f"Creating index {index_name}...") 47 | IndexNameStructure(index_name=index_name) 48 | pc = Pinecone(api_key=os.getenv('PINECONE_API_KEY')) 49 | existing_indexes = [index_info["name"] for index_info in pc.list_indexes()] 50 | if index_name in existing_indexes: 51 | raise Exception("The index already exists...") 52 | pc.create_index( 53 | name=index_name.lower(), 54 | dimension=dimension, 55 | metric=metric, 56 | spec=ServerlessSpec(cloud="aws", region="us-east-1"), 57 | ) 58 | 59 | while not pc.describe_index(index_name).status["ready"]: 60 | time.sleep(1) 61 | 62 | logger.info(f"Index '{index_name}' created...") 63 | 64 | PineconeVectorStore.from_documents(documents = docs, embedding = embedding, index_name = index_name) 65 | 66 | logger.info(f"Index '{index_name}' populated with data...") 67 | 68 | def loading_vdb(self, index_name: str, embedding=OpenAIEmbeddings(model="text-embedding-ada-002")): 69 | logger.info("Loading vector database from Pinecone...") 70 | self.vdb = PineconeVectorStore(index_name=index_name, embedding=embedding) 71 | logger.info("Vector database loaded...") 72 | 73 | 74 | def adding_documents(self, new_info: Dict[str,str]): 75 | ExpectedNewData(new_info = new_info) 76 | logger.info("Adding data in the vector database...") 77 | self.vdb.add_documents([Document(page_content="question: " + new_info['question'] + '\n answer: ' + new_info['answer'], metadata={"question": new_info['question']})]) 78 | logger.info("More info added in the vector database...") 79 | 80 | def finding_similar_docs(self, user_query): 81 | docs = self.vdb.similarity_search_with_relevance_scores( 82 | query = user_query, 83 | k = 3, 84 | score_threshold=0.9 85 | ) 86 | 87 | return docs 88 | 89 | 90 | -------------------------------------------------------------------------------- /data/syntetic_data/get_availability.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR = os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | import csv 11 | from datetime import datetime, timedelta 12 | import random 13 | 14 | # Define the data structure 15 | data = [ 16 | {"specialization": "general_dentist", "dentists": [{"name": "john doe"}, {"name": "emily johnson"}]}, 17 | {"specialization": "cosmetic_dentist", "dentists": [{"name": "jane smith"}, {"name": "lisa brown"}]}, 18 | {"specialization": "prosthodontist", "dentists": [{"name": "michael green"}]}, 19 | {"specialization": "pediatric_dentist", "dentists": [{"name": "sarah wilson"}]}, 20 | {"specialization": "emergency_dentist", "dentists": [{"name": "daniel miller"}, {"name": "susan davis"}]}, 21 | {"specialization": "oral_surgeon", "dentists": [{"name": "robert martinez"}]}, 22 | {"specialization": "orthodontist", "dentists": [{"name": "kevin anderson"}]}, 23 | ] 24 | 25 | 26 | # Function to generate time slots 27 | def generate_time_slots(start_time, end_time, interval_minutes): 28 | current_time = start_time 29 | time_slots = [] 30 | while current_time < end_time: 31 | time_slots.append(current_time.strftime("%Y-%m-%d %H:%M")) 32 | current_time += timedelta(minutes=interval_minutes) 33 | return time_slots 34 | 35 | 36 | # Generate CSV data 37 | def generate_csv(filename): 38 | # Get the current date 39 | current_date = datetime.now().date() 40 | start_date = datetime(current_date.year, current_date.month, current_date.day) 41 | 42 | # Define the time slots for the month 43 | time_slots = [] 44 | for day in range(30): # Covering one month 45 | date = start_date + timedelta(days=day) 46 | if date.weekday() < 5: # Monday to Friday 47 | time_slots += generate_time_slots( 48 | datetime(date.year, date.month, date.day, 8, 0), 49 | datetime(date.year, date.month, date.day, 17, 0), 50 | 30, 51 | ) 52 | elif date.weekday() == 5: # Saturday 53 | time_slots += generate_time_slots( 54 | datetime(date.year, date.month, date.day, 9, 0), 55 | datetime(date.year, date.month, date.day, 13, 0), 56 | 30, 57 | ) 58 | 59 | # Mark the first two days as unavailable 60 | unavailable_slots = [] 61 | for day in range(2): 62 | unavailable_date = start_date + timedelta(days=day) 63 | for slot in time_slots: 64 | if unavailable_date.strftime("%Y-%m-%d") in slot: 65 | unavailable_slots.append(slot) 66 | 67 | with open(filename, mode="w", newline="") as file: 68 | writer = csv.writer(file) 69 | writer.writerow(["date_slot", "specialization", "doctor_name", "is_available", "patient_to_attend"]) 70 | 71 | for specialization in data: 72 | for dentist in specialization["dentists"]: 73 | for slot in time_slots: 74 | if slot in unavailable_slots: 75 | is_available = False # Unavailable for the first two days 76 | else: 77 | # Randomly assign availability (70% chance of being available) 78 | is_available = random.choice([True] * 7 + [False] * 3) 79 | 80 | patient_to_attend = None if is_available else random.randint(1000000, 1000100) 81 | writer.writerow([slot, specialization["specialization"], dentist["name"], is_available, patient_to_attend]) 82 | 83 | if __name__ == '__main__': 84 | generate_csv(f"{WORKDIR}/data/syntetic_data/availability.csv") -------------------------------------------------------------------------------- /data/catalog.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "specialization": "General Dentist", 4 | "responsibilities": [ 5 | "Performs routine check-ups, cleanings, exams, and preventive care.", 6 | "Handles basic restorative work such as fillings and extractions.", 7 | "Conducts oral cancer screenings and provides fluoride treatments." 8 | ], 9 | "dentists": [ 10 | { 11 | "name": "John Doe", 12 | "license_number": "GD123456" 13 | }, 14 | { 15 | "name": "Emily Johnson", 16 | "license_number": "GD654321" 17 | } 18 | ] 19 | }, 20 | { 21 | "specialization": "Cosmetic Dentist", 22 | "responsibilities": [ 23 | "Specializes in improving the appearance of teeth and smiles.", 24 | "Performs procedures such as teeth whitening, veneers, and bonding.", 25 | "Offers Invisalign and other cosmetic alignment solutions." 26 | ], 27 | "dentists": [ 28 | { 29 | "name": "Jane Smith", 30 | "license_number": "CD234567" 31 | }, 32 | { 33 | "name": "Lisa Brown", 34 | "license_number": "CD765432" 35 | } 36 | ] 37 | }, 38 | { 39 | "specialization": "Prosthodontist", 40 | "responsibilities": [ 41 | "Specializes in restorative treatments like crowns, bridges, and dentures.", 42 | "Manages complex dental restorations and dental implants.", 43 | "Focuses on restoring the function and aesthetics of the oral cavity." 44 | ], 45 | "dentists": [ 46 | { 47 | "name": "Michael Green", 48 | "license_number": "PR345678" 49 | } 50 | ] 51 | }, 52 | { 53 | "specialization": "Pediatric Dentist (Pedodontist)", 54 | "responsibilities": [ 55 | "Provides dental care tailored to children and teenagers.", 56 | "Ensures a comfortable and positive experience for young patients.", 57 | "Focuses on preventive care and education for children." 58 | ], 59 | "dentists": [ 60 | { 61 | "name": "Sarah Wilson", 62 | "license_number": "PD456789" 63 | } 64 | ] 65 | }, 66 | { 67 | "specialization": "Emergency Dentist", 68 | "responsibilities": [ 69 | "Handles urgent dental issues, including toothaches and broken teeth.", 70 | "Provides immediate care for dental emergencies." 71 | ], 72 | "dentists": [ 73 | { 74 | "name": "Daniel Miller", 75 | "license_number": "ED567890" 76 | }, 77 | { 78 | "name": "Susan Davis", 79 | "license_number": "ED098765" 80 | } 81 | ] 82 | }, 83 | { 84 | "specialization": "Oral Surgeon", 85 | "responsibilities": [ 86 | "Performs complex extractions and surgical procedures.", 87 | "Handles implant placements and other oral surgeries." 88 | ], 89 | "dentists": [ 90 | { 91 | "name": "Robert Martinez", 92 | "license_number": "OS678901" 93 | } 94 | ] 95 | }, 96 | { 97 | "specialization": "Orthodontist", 98 | "responsibilities": [ 99 | "Specializes in the alignment of teeth and jaws using braces or Invisalign.", 100 | "Manages the development of facial growth and dental development." 101 | ], 102 | "dentists": [ 103 | { 104 | "name": "Kevin Anderson", 105 | "license_number": "OR789012" 106 | } 107 | ] 108 | } 109 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/availability.csv 2 | 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR=os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | from langchain_core.messages import HumanMessage, SystemMessage 11 | from langgraph.checkpoint.memory import MemorySaver 12 | from langgraph.graph import END, StateGraph, MessagesState 13 | from langgraph.prebuilt import ToolNode 14 | from typing import TypedDict, Annotated, List, Literal 15 | from langchain_core.messages import AnyMessage, HumanMessage 16 | import operator 17 | from src.validators.agent_validators import * 18 | from src.agent_tools import check_availability_by_doctor, check_availability_by_specialization, check_results, set_appointment, cancel_appointment, reminder_appointment, reschedule_appointment, retrieve_faq_info, get_catalog_specialists, obtain_specialization_by_doctor 19 | from datetime import datetime 20 | from src.utils import get_model 21 | import logging 22 | import logging_config 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | class MessagesState(TypedDict): 27 | messages: Annotated[List[AnyMessage], operator.add] 28 | 29 | tools = [obtain_specialization_by_doctor, check_availability_by_doctor, check_availability_by_specialization, cancel_appointment, get_catalog_specialists, retrieve_faq_info, set_appointment, reminder_appointment, check_results,reschedule_appointment, reschedule_appointment] 30 | 31 | tool_node = ToolNode(tools) 32 | 33 | 34 | model = get_model('meta') 35 | model = model.bind_tools(tools = tools) 36 | 37 | def should_continue(state: MessagesState) -> Literal["tools", "human_feedback"]: 38 | messages = state['messages'] 39 | last_message = messages[-1] 40 | if last_message.tool_calls: 41 | return "tools" 42 | return "human_feedback" 43 | 44 | #The commented part is because it breaks the UI with the input function 45 | def should_continue_with_feedback(state: MessagesState) -> Literal["agent", "end"]: 46 | messages = state['messages'] 47 | last_message = messages[-1] 48 | if isinstance(last_message, dict): 49 | if last_message.get("type","") == 'human': 50 | return "agent" 51 | if (isinstance(last_message, HumanMessage)): 52 | return "agent" 53 | return "end" 54 | 55 | 56 | def call_model(state: MessagesState): 57 | messages = [SystemMessage(content=f"You are helpful assistant in Ovide Clinic, dental care center in California (United States).\nAs reference, today is {datetime.now().strftime('%Y-%m-%d %H:%M, %A')}.\nKeep a friendly, professional tone.\nAvoid verbosity.\nConsiderations:\n- Don´t assume parameters in call functions that it didnt say.\n- MUST NOT force users how to write. Let them write in the way they want.\n- The conversation should be very natural like a secretary talking with a client.\n- Call only ONE tool at a time.")] + state['messages'] 58 | response = model.invoke(messages) 59 | return {"messages": [response]} 60 | 61 | def read_human_feedback(state: MessagesState): 62 | pass 63 | 64 | 65 | workflow = StateGraph(MessagesState) 66 | 67 | workflow.add_node("agent", call_model) 68 | workflow.add_node("tools", tool_node) 69 | workflow.add_node("human_feedback", read_human_feedback) 70 | workflow.set_entry_point("agent") 71 | workflow.add_conditional_edges( 72 | "agent", 73 | should_continue, 74 | {"human_feedback": 'human_feedback', 75 | "tools": "tools"} 76 | ) 77 | workflow.add_conditional_edges( 78 | "human_feedback", 79 | should_continue_with_feedback, 80 | {"agent": 'agent', 81 | "end": END} 82 | ) 83 | workflow.add_edge("tools", 'agent') 84 | 85 | checkpointer = MemorySaver() 86 | 87 | app = workflow.compile(checkpointer=checkpointer, 88 | interrupt_before=['human_feedback']) 89 | 90 | if __name__ == '__main__': 91 | while True: 92 | question = input("Put your question: ") 93 | 94 | for event in app.stream( 95 | {"messages": [ 96 | HumanMessage(content=question) 97 | ]}, 98 | config={"configurable": {"thread_id": 42}} 99 | ): 100 | if event.get("agent","") == "": 101 | continue 102 | else: 103 | msg = event['agent']['messages'][-1].content 104 | if msg == '': 105 | continue 106 | else: 107 | print(msg) 108 | 109 | -------------------------------------------------------------------------------- /data/syntetic_data/patients_info.csv: -------------------------------------------------------------------------------- 1 | id_number,name,location,age,birth_date,sex 2 | 1000000,Emma Johnson,Los Angeles,28,1996-06-19,F 3 | 1000002,Liam Smith,San Francisco,34,1989-03-22,M 4 | 1000003,Olivia Brown,San Diego,45,1978-01-10,F 5 | 1000004,Noah Davis,San Jose,29,1994-11-30,M 6 | 1000005,Ava Garcia,Sacramento,31,1992-07-05,F 7 | 1000006,Isabella Martinez,Fresno,24,1999-05-16,F 8 | 1000007,Lucas Rodriguez,Long Beach,37,1986-12-09,M 9 | 1000008,Sophia Hernandez,Oakland,50,1973-08-21,F 10 | 1000009,Mason Lopez,Bakersfield,42,1981-02-17,M 11 | 1000010,Charlotte Wilson,Anaheim,26,1997-04-14,F 12 | 1000011,Amelia Anderson,Santa Ana,33,1990-09-12,F 13 | 1000012,Ethan Thomas,Chula Vista,39,1984-10-30,M 14 | 1000013,Harper Taylor,San Bernardino,27,1996-01-19,F 15 | 1000014,James Moore,Riverside,36,1987-03-11,M 16 | 1000015,Abigail Jackson,Stockton,30,1993-06-25,F 17 | 1000016,Alexander White,Chico,41,1982-02-05,M 18 | 1000017,Emily Harris,Modesto,38,1985-07-28,F 19 | 1000018,Daniel Martin,Palm Springs,32,1991-11-02,M 20 | 1000019,Sofia Thompson,Hayward,29,1994-08-15,F 21 | 1000020,Michael Garcia,Sunnyvale,44,1979-12-12,M 22 | 1000021,Scarlett Martinez,Pasadena,25,1998-05-30,F 23 | 1000022,Henry Robinson,Fullerton,35,1988-04-20,M 24 | 1000023,Grace Clark,Orange,40,1983-09-23,F 25 | 1000024,Jackson Lewis,Visalia,46,1977-02-14,M 26 | 1000025,Chloe Lee,Elk Grove,28,1995-06-18,F 27 | 1000026,David Walker,Salinas,34,1989-01-24,M 28 | 1000027,Victoria Hall,San Mateo,47,1976-03-09,F 29 | 1000028,Joseph Allen,Carlsbad,39,1984-10-28,M 30 | 1000029,Zoey Young,Merced,30,1993-12-01,F 31 | 1000030,Samuel King,Redwood City,42,1981-07-15,M 32 | 1000031,Aria Wright,Mountain View,26,1997-08-05,F 33 | 1000032,Matthew Scott,Westminster,33,1990-11-22,M 34 | 1000033,Lucy Torres,Richmond,29,1994-04-10,F 35 | 1000034,Andrew Nguyen,Chino,38,1985-05-03,M 36 | 1000035,Ellie Hill,South Gate,31,1992-02-17,F 37 | 1000036,Christopher Adams,San Marcos,48,1975-06-30,M 38 | 1000037,Stella Baker,San Leandro,25,1998-09-14,F 39 | 1000038,Joshua Gonzalez,La Habra,36,1987-12-02,M 40 | 1000039,Penelope Nelson,Temecula,41,1982-03-20,F 41 | 1000040,Jack Carter,Clovis,30,1993-01-29,M 42 | 1000041,Avery Mitchell,Union City,27,1996-07-22,F 43 | 1000042,Leo Perez,Antioch,34,1989-10-11,M 44 | 1000043,Archer Roberts,San Rafael,39,1984-04-16,M 45 | 1000044,Addison Turner,Compton,32,1991-05-28,F 46 | 1000045,Isaac Phillips,San Bruno,45,1978-11-09,M 47 | 1000046,Milana Campbell,Menifee,28,1995-06-04,F 48 | 1000047,Caleb Parker,San Jose,35,1988-08-17,M 49 | 1000048,Layla Evans,Oakland,42,1981-01-31,F 50 | 1000049,Wyatt Edwards,Chula Vista,29,1994-03-25,M 51 | 1000050,Naomi Collins,Modesto,43,1980-12-08,F 52 | 1000051,Daniel Murphy,Los Angeles,34,1989-07-19,M 53 | 1000052,Victoria Rivera,San Diego,27,1996-10-30,F 54 | 1000053,James Bennett,San Francisco,38,1985-09-02,M 55 | 1000054,Leah Wood,Long Beach,32,1991-02-24,F 56 | 1000055,Henry Cooper,San Bernardino,29,1994-11-14,M 57 | 1000056,Abigail Reed,Stockton,36,1987-03-06,F 58 | 1000057,Leo Morgan,Sacramento,45,1978-05-17,M 59 | 1000058,Isabella Bell,Fresno,30,1993-08-21,F 60 | 1000059,Lucas Murphy,Chico,41,1982-12-27,M 61 | 1000060,Emma Gomez,Hayward,26,1997-04-15,F 62 | 1000061,Noah Brooks,Chino,38,1985-01-11,M 63 | 1000062,Chloe Kelly,Carlsbad,32,1991-06-09,F 64 | 1000063,Jackson Sanders,Sunnyvale,43,1980-03-18,M 65 | 1000064,Scarlett Price,Elk Grove,29,1994-10-12,F 66 | 1000065,Oliver Butler,Richmond,35,1988-08-22,M 67 | 1000066,Grace Bennett,Chula Vista,31,1992-07-07,F 68 | 1000067,Mason Morris,San Marcos,48,1975-12-30,M 69 | 1000068,Lily Rivera,San Leandro,28,1995-05-26,F 70 | 1000069,Samuel Diaz,Visalia,39,1984-01-03,M 71 | 1000070,Emily Torres,Antioch,34,1989-09-15,F 72 | 1000071,Daniel Hughes,San Bruno,42,1981-11-19,M 73 | 1000072,Amelia Ramirez,Menifee,30,1993-06-02,F 74 | 1000073,Caleb Foster,San Jose,35,1988-02-22,M 75 | 1000074,Zoey James,Los Angeles,29,1994-08-05,F 76 | 1000075,Henry Sanchez,San Francisco,36,1987-12-14,M 77 | 1000076,Addison Watson,San Diego,31,1992-03-30,F 78 | 1000077,Leo Cooper,Long Beach,44,1979-01-17,M 79 | 1000078,Chloe Price,Chico,25,1998-11-26,F 80 | 1000079,Isaac Bennett,Modesto,38,1985-07-03,M 81 | 1000080,Harper Reed,Oakland,32,1991-10-16,F 82 | 1000081,Joshua Hughes,Chula Vista,39,1984-04-12,M 83 | 1000082,Layla Murphy,Carlsbad,30,1993-05-21,F 84 | 1000083,Oliver Gonzalez,Stockton,45,1978-06-09,M 85 | 1000084,Stella Martinez,San Bernardino,28,1995-09-20,F 86 | 1000085,James Smith,San Jose,34,1989-02-02,M 87 | 1000086,Emily Taylor,Los Angeles,27,1996-01-04,F 88 | 1000087,Daniel Clark,Sacramento,35,1988-08-09,M 89 | 1000088,Victoria Lewis,Fresno,42,1981-03-17,F 90 | 1000089,Henry Walker,Hayward,30,1993-05-14,M 91 | 1000090,Chloe Hall,Antioch,29,1994-12-22,F 92 | 1000091,Leo Young,San Rafael,36,1987-06-18,M 93 | 1000092,Addison Allen,Menifee,31,1992-11-30,F 94 | 1000093,Isaac Martin,Chino,44,1979-02-01,M 95 | 1000094,Zoey Nelson,San Diego,28,1995-04-10,F 96 | 1000095,James Carter,Long Beach,39,1984-10-15,M 97 | 1000096,Emma Scott,San Francisco,34,1989-07-20,F 98 | 1000097,Daniel Turner,Los Angeles,30,1993-03-08,M 99 | 1000098,Olivia Ramirez,Modesto,42,1981-05-25,F 100 | 1000099,Henry Phillips,Chico,29,1994-09-11,M 101 | 1000100,Amelia Evans,San Bernardino,36,1987-11-29,F -------------------------------------------------------------------------------- /faq/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "question": "When is the clinic open?", 4 | "answer": "We are open Monday through Friday from 8:00 AM to 5:00 PM, and Saturday from 9:00 AM to 1:00 PM. We are closed on Sundays and major holidays." 5 | }, 6 | { 7 | "question": "Do you accept walk-ins?", 8 | "answer": "While we encourage appointments to minimize wait times, we do our best to accommodate walk-ins whenever possible. However, patients with scheduled appointments will be prioritized." 9 | }, 10 | { 11 | "question": "What insurance plans do you accept?", 12 | "answer": "We accept most major dental insurance plans, including Delta Dental, Aetna, MetLife, Cigna, and UnitedHealthcare. We also work with many other PPO plans. Please provide your insurance information when scheduling your appointment so we can verify your coverage." 13 | }, 14 | { 15 | "question": "Are you accepting new patients?", 16 | "answer": "Yes, we are always happy to welcome new patients to our practice." 17 | }, 18 | { 19 | "question": "Do you offer emergency dental services?", 20 | "answer": "Yes, we offer emergency dental services during our regular operating hours. If you are experiencing a dental emergency, please call our office immediately (405 910 189 291), or come to the clinic and we will do our best to see you as soon as possible." 21 | }, 22 | { 23 | "question": "What should I do if I have a dental emergency outside of operating hours?", 24 | "answer": "If you have a dental emergency outside of our operating hours, please call our office (405 910 189 291). Our answering service will provide you with instructions on how to reach the on-call dentist." 25 | }, 26 | { 27 | "question": "How often should I have a dental check-up?", 28 | "answer": "We recommend having a dental check-up and cleaning every six months to maintain optimal oral health. However, depending on your individual needs, we may recommend more frequent visits." 29 | }, 30 | { 31 | "question": "What is your policy on missed appointments?", 32 | "answer": "We understand that things happen, but we kindly request that you provide us with at least 24 hours' notice if you need to reschedule or cancel your appointment. Missed appointments without proper notification may be subject to a fee of $50." 33 | }, 34 | { 35 | "question": "Do you offer sedation dentistry?", 36 | "answer": "Yes, we offer various sedation dentistry options, including nitrous oxide (laughing gas) and oral conscious sedation, to help patients feel more relaxed and comfortable during dental procedures. We will discuss which option is best for you based on your individual needs and anxiety levels." 37 | }, 38 | { 39 | "question": "Are your dental products environmentally friendly?", 40 | "answer": "Yes, we are committed to using environmentally friendly dental products whenever possible. We use digital X-rays to reduce radiation exposure and minimize waste. We also choose products and materials that are biocompatible and sustainable." 41 | }, 42 | { 43 | "question": "How much does the first session cost?", 44 | "answer": "The cost of the first session typically ranges from $100 to $200, depending on the services required. This usually includes a comprehensive examination, X-rays, and a cleaning. We will provide you with a detailed breakdown of the costs before any treatment is performed." 45 | }, 46 | { 47 | "question": "How is pricing determined for different dental specialities?", 48 | "answer": "Pricing for different dental specialties is determined by factors such as the complexity of the procedure, the time required, the materials used, and the expertise of the dentist. We will provide you with a clear explanation of the costs associated with your specific treatment plan." 49 | }, 50 | { 51 | "question": "What forms of payment do you accept?", 52 | "answer": "We accept cash, checks, major credit cards (Visa, Mastercard, Discover, American Express), and debit cards." 53 | }, 54 | { 55 | "question": "Do you offer discounts for senior citizens or students?", 56 | "answer": "No" 57 | }, 58 | { 59 | "question": "Are consultations free?", 60 | "answer": "Yes, initial consultations are free of charge. This allows you to meet our team, discuss your dental concerns, and receive a preliminary assessment." 61 | }, 62 | { 63 | "question": "Is there parking available at the clinic?", 64 | "answer": "Yes, we have free parking available for our patients in our dedicated parking lot." 65 | }, 66 | { 67 | "question": "What technology do you use in your clinic?", 68 | "answer": "We utilize the latest dental technology, including digital X-rays (which reduce radiation exposure), intraoral cameras (which allow you to see inside your mouth), and laser dentistry (which offers minimally invasive treatment options). We are committed to staying at the forefront of advancements in dental technology to provide the best possible care for our patients." 69 | }, 70 | { 71 | "question": "Do you have experience treating patients with dental anxiety?", 72 | "answer": "Yes, our team is experienced and compassionate in treating patients with dental anxiety. We understand that dental visits can be stressful for some, and we strive to create a relaxing and comfortable environment. We offer various options to help manage dental anxiety, including sedation dentistry, calming music, and a gentle approach. We will work with you to understand your concerns and develop a personalized treatment plan that addresses your specific needs." 73 | }, 74 | { 75 | "question": "What do I need to bring with me for the visit?", 76 | "answer": "For your first visit, please bring: \n- Your insurance card (if applicable)\n- A photo ID\n- A list of any medications you are currently taking\n- Any relevant medical or dental history information\n- If you have dental records from a previous dentist, you are welcome to bring those as well, though it is not required." 77 | }, 78 | { 79 | "question": "What is the address?", 80 | "answer": "Our address is 901 Humeh Street, Teoville, CA 90210." 81 | }, 82 | { 83 | "question": "Do you offer teeth whitening?", 84 | "answer": "Yes, we offer both in-office and take-home teeth whitening options." 85 | }, 86 | { 87 | "question": "How long do fillings last?", 88 | "answer": "The lifespan of a filling depends on the material used and how well you care for your teeth. With proper care, fillings can last for many years." 89 | }, 90 | { 91 | "question": "How much does a dental cleaning cost?", 92 | "answer": "The cost of a dental cleaning varies depending on your individual needs and insurance coverage. We will provide you with a cost estimate before any treatment is performed." 93 | }, 94 | { 95 | "question": "What are your COVID-19 safety protocols?", 96 | "answer": "We follow strict COVID-19 safety protocols, including enhanced cleaning and disinfection procedures, social distancing measures, and mandatory mask-wearing for staff and patients." 97 | }, 98 | { 99 | "question": "Do you have a website?", 100 | "answer": "Yes, our website is www.caovideclinic.org. You can find more information about our services, our team, and our practice on our website." 101 | }, 102 | { 103 | "question": "Do you offer payment plans?", 104 | "answer": "Yes, we offer flexible payment plans to help make your dental care more affordable. We work with Hija Bank, which offers loans with low rates if you make treatments with us. If you need it, contact them directly." 105 | }, 106 | { 107 | "question": "What should I expect during my first visit?", 108 | "answer": "During your first visit, we will review your medical and dental history, take X-rays, and perform a comprehensive exam. We will then discuss your treatment options and answer any questions you may have." 109 | }, 110 | { 111 | "question":"When and how can I access my dental study results?", 112 | "answer":"Your dental study results will be available around 5 business days after your appointment. When they are ready, you can access them in the following ways:\n- Through our secure online portal: www.caovideclinic.org\n- Coming to our clinic whenever you want in our attention time." 113 | } 114 | ] -------------------------------------------------------------------------------- /src/agent_tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import sys 4 | 5 | load_dotenv() 6 | WORKDIR=os.getenv("WORKDIR") 7 | os.chdir(WORKDIR) 8 | sys.path.append(WORKDIR) 9 | 10 | from langchain_core.tools import tool 11 | from src.validators.agent_validators import * 12 | from typing import Literal 13 | import pandas as pd 14 | import json 15 | from src.vector_database.main import PineconeManagment 16 | from src.utils import format_retrieved_docs 17 | 18 | pinecone_conn = PineconeManagment() 19 | pinecone_conn.loading_vdb(index_name = 'ovidedentalclinic') 20 | retriever = pinecone_conn.vdb.as_retriever(search_type="similarity", 21 | search_kwargs={"k": 2}) 22 | rag_chain = retriever | format_retrieved_docs 23 | 24 | #All the tools to consider 25 | @tool 26 | def check_availability_by_doctor(desired_date:DateModel, doctor_name:Literal['kevin anderson','robert martinez','susan davis','daniel miller','sarah wilson','michael green','lisa brown','jane smith','emily johnson','john doe']): 27 | """ 28 | Checking the database if we have availability for the specific doctor. 29 | The parameters should be mentioned by the user in the query 30 | """ 31 | #Dummy data 32 | df = pd.read_csv(f"{WORKDIR}/data/syntetic_data/availability.csv") 33 | df['date_slot_time'] = df['date_slot'].apply(lambda input: input.split(' ')[-1]) 34 | rows = list(df[(df['date_slot'].apply(lambda input: input.split(' ')[0]) == desired_date.date)&(df['doctor_name'] == doctor_name)&(df['is_available'] == True)]['date_slot_time']) 35 | 36 | if len(rows) == 0: 37 | output = "No availability in the entire day" 38 | else: 39 | output = f'This availability for {desired_date.date}\n' 40 | output += "Available slots: " + ', '.join(rows) 41 | 42 | return output 43 | 44 | 45 | def check_availability_by_specialization(desired_date:DateModel, specialization:Literal["general_dentist", "cosmetic_dentist", "prosthodontist", "pediatric_dentist","emergency_dentist","oral_surgeon","orthodontist"]): 46 | """ 47 | Checking the database if we have availability for the specific specialization. 48 | The parameters should be mentioned by the user in the query 49 | """ 50 | #Dummy data 51 | df = pd.read_csv(f"{WORKDIR}/data/syntetic_data/availability.csv") 52 | df['date_slot_time'] = df['date_slot'].apply(lambda input: input.split(' ')[-1]) 53 | rows = df[(df['date_slot'].apply(lambda input: input.split(' ')[0]) == desired_date.date) & (df['specialization'] == specialization) & (df['is_available'] == True)].groupby(['specialization', 'doctor_name'])['date_slot_time'].apply(list).reset_index(name='available_slots') 54 | 55 | if len(rows) == 0: 56 | output = "No availability in the entire day" 57 | else: 58 | output = f'This availability for {desired_date.date}\n' 59 | for row in rows.values: 60 | output += row[1] + ". Available slots: " + ', '.join(row[2])+'\n' 61 | 62 | return output 63 | 64 | @tool 65 | def reschedule_appointment(old_date:DateTimeModel, new_date:DateTimeModel, id_number:IdentificationNumberModel, doctor_name:Literal['kevin anderson','robert martinez','susan davis','daniel miller','sarah wilson','michael green','lisa brown','jane smith','emily johnson','john doe']): 66 | """ 67 | Rescheduling an appointment. 68 | The parameters MUST be mentioned by the user in the query. 69 | """ 70 | #Dummy data 71 | df = pd.read_csv(f'{WORKDIR}/data/syntetic_data/availability.csv') 72 | available_for_desired_date = df[(df['date_slot'] == new_date.date)&(df['is_available'] == True)&(df['doctor_name'] == doctor_name)] 73 | if len(available_for_desired_date) == 0: 74 | return "Not available slots in the desired period" 75 | else: 76 | cancel_appointment.invoke({'date':old_date, 'id_number':id_number, 'doctor_name':doctor_name}) 77 | set_appointment.invoke({'desired_date':new_date, 'id_number': id_number, 'doctor_name': doctor_name}) 78 | return "Succesfully rescheduled for the desired time" 79 | 80 | @tool 81 | def cancel_appointment(date:DateTimeModel, id_number:IdentificationNumberModel, doctor_name:Literal['kevin anderson','robert martinez','susan davis','daniel miller','sarah wilson','michael green','lisa brown','jane smith','emily johnson','john doe']): 82 | """ 83 | Canceling an appointment. 84 | The parameters MUST be mentioned by the user in the query. 85 | """ 86 | df = pd.read_csv(f'{WORKDIR}/data/syntetic_data/availability.csv') 87 | case_to_remove = df[(df['date_slot'] == date.date)&(df['patient_to_attend'] == id_number.id)&(df['doctor_name'] == doctor_name)] 88 | if len(case_to_remove) == 0: 89 | return "You don´t have any appointment with that specifications" 90 | else: 91 | df.loc[(df['date_slot'] == date.date) & (df['patient_to_attend'] == id_number.id) & (df['doctor_name'] == doctor_name), ['is_available', 'patient_to_attend']] = [True, None] 92 | df.to_csv(f'{WORKDIR}/data/syntetic_data/availability.csv', index = False) 93 | 94 | return "Succesfully cancelled" 95 | 96 | @tool 97 | def get_catalog_specialists(): 98 | """ 99 | Obtain information about the doctors and specializations/services we provide. 100 | The parameters MUST be mentioned by the user in the query 101 | """ 102 | with open(f"{WORKDIR}/data/catalog.json","r") as file: 103 | file = json.loads(file.read()) 104 | 105 | return file 106 | 107 | @tool 108 | def set_appointment(desired_date:DateTimeModel, id_number:IdentificationNumberModel, doctor_name:Literal['kevin anderson','robert martinez','susan davis','daniel miller','sarah wilson','michael green','lisa brown','jane smith','emily johnson','john doe']): 109 | """ 110 | Set appointment with the doctor. 111 | The parameters MUST be mentioned by the user in the query. 112 | """ 113 | df = pd.read_csv(f'{WORKDIR}/data/syntetic_data/availability.csv') 114 | case = df[(df['date_slot'] == desired_date.date)&(df['doctor_name'] == doctor_name)&(df['is_available'] == True)] 115 | if len(case) == 0: 116 | return "No available appointments for that particular case" 117 | else: 118 | df.loc[(df['date_slot'] == desired_date.date)&(df['doctor_name'] == doctor_name) & (df['is_available'] == True), ['is_available','patient_to_attend']] = [False, id_number.id] 119 | 120 | df.to_csv(f'{WORKDIR}/data/syntetic_data/availability.csv', index = False) 121 | 122 | return "Succesfully done" 123 | 124 | @tool 125 | def check_results(id_number:IdentificationNumberModel): 126 | """ 127 | Check if the result of the pacient is available. 128 | The parameters MUST be mentioned by the user in the query 129 | """ 130 | #Dummy data 131 | df = pd.read_csv(f'{WORKDIR}/data/syntetic_data/studies_status.csv') 132 | rows = df[(df['patient_id'] == id_number.id)][['medical_study','is_available']] 133 | if len(rows) == 0: 134 | return "The patient doesn´t have any study made" 135 | else: 136 | return rows 137 | 138 | @tool 139 | def reminder_appointment(id_number:IdentificationNumberModel): 140 | """ 141 | Returns when the pacient has its appointment with the doctor 142 | The parameters MUST be mentioned by the user in the query 143 | """ 144 | df = pd.read_csv(f'{WORKDIR}/data/syntetic_data/availability.csv') 145 | rows = df[(df['patient_to_attend'] == id_number.id)][['time_slot','doctor_name','specialization']] 146 | if len(rows) == 0: 147 | return "The patient doesn´t have any appointment yet" 148 | else: 149 | return rows 150 | 151 | 152 | @tool 153 | def retrieve_faq_info(question:str): 154 | """ 155 | Retrieve documents or additional info from general questions about the medical clinic. 156 | Call this tool if question is regarding center: 157 | For example: is it open? Do you have parking? Can I go with bike? etc... 158 | """ 159 | return rag_chain.invoke(question) 160 | 161 | @tool 162 | def obtain_specialization_by_doctor(doctor_name:Literal['kevin anderson','robert martinez','susan davis','daniel miller','sarah wilson','michael green','lisa brown','jane smith','emily johnson','john doe']): 163 | """ 164 | Retrieve which specialization covers a specific doctor. 165 | Use this internal tool if you need more information about a doctor for setting an appointment. 166 | """ 167 | with open(f"{WORKDIR}/data/catalog.json","r") as file: 168 | catalog = json.loads(file.read()) 169 | 170 | return str([{specialization['specialization']: [dentist['name'] for dentist in specialization['dentists']]} for specialization in catalog]) 171 | --------------------------------------------------------------------------------