├── .editorconfig ├── .github └── workflows │ └── build_and_deploy.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── agents ├── .gitkeep └── l4m_agent.py ├── app.py ├── deployment ├── README.md ├── helm │ ├── Chart.yaml │ ├── templates │ │ ├── ingress.yaml │ │ ├── letsencrypt-cert-issuer.yaml │ │ ├── secrets.yaml │ │ └── streamlit.yaml │ └── values.yaml └── k8s │ └── configmap.yaml ├── environment.yaml ├── nbs ├── .gitkeep ├── 23-05-18_mercantile-tool.ipynb ├── 23-05-19_geopy-tool.ipynb ├── 23-05-26_osmnx-tool.ipynb └── 23-06-28_stac-tool.ipynb └── tools ├── .gitkeep ├── geopy ├── __init__.py ├── distance.py └── geocode.py ├── mercantile_tool.py ├── osmnx ├── geometry.py └── network.py └── stac └── search.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Docstrings and comments use max_line_length = 79 14 | [*.py] 15 | max_line_length = 119 16 | 17 | # Use 2 spaces for the HTML files 18 | [*.html] 19 | indent_size = 2 20 | 21 | # The JSON files contain newlines inconsistently 22 | [*.json] 23 | indent_size = 2 24 | insert_final_newline = ignore 25 | 26 | [**/admin/js/vendor/**] 27 | indent_style = ignore 28 | indent_size = ignore 29 | 30 | # Minified JavaScript files shouldn't be changed 31 | [**.min.js] 32 | indent_style = ignore 33 | insert_final_newline = ignore 34 | 35 | # Makefiles always use tabs for indentation 36 | [Makefile] 37 | indent_style = tab 38 | 39 | # Batch files use tabs for indentation 40 | [*.bat] 41 | indent_style = tab 42 | 43 | [docs/**.txt] 44 | max_line_length = 79 45 | 46 | [*.yml] 47 | indent_size = 2 48 | -------------------------------------------------------------------------------- /.github/workflows/build_and_deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build Docker Image, Push to GHCR and Deploy to GKE 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - feat/deployment 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v2 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v4 29 | with: 30 | context: . 31 | push: true 32 | tags: | 33 | ghcr.io/developmentseed/llllm:latest 34 | ghcr.io/developmentseed/llllm:${{ github.sha }} 35 | cache-from: type=gha 36 | cache-to: type=gha,mode=max 37 | 38 | - id: 'auth' 39 | uses: 'google-github-actions/auth@v1' 40 | with: 41 | credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' 42 | - name: 'Set up Cloud SDK' 43 | uses: 'google-github-actions/setup-gcloud@v1' 44 | - name: Configure kubectl 45 | run: | 46 | gcloud components install gke-gcloud-auth-plugin 47 | gcloud config set project ${{ secrets.PROJECT_ID }} 48 | gcloud config set compute/zone us-central1-f 49 | gcloud container clusters get-credentials ${{ secrets.CLUSTER_NAME }} 50 | - name: 'Set up Helm' 51 | uses: 'Azure/setup-helm@v1' 52 | with: 53 | version: 'v3.12.0' 54 | - name: "Deploy Helm Chart" 55 | run: | 56 | helm upgrade --install project-llllm deployment/helm --create-namespace --namespace project-llllm \ 57 | --set secrets.openai_api_key=${{ secrets.OPENAI_API_KEY }} \ 58 | --set streamlit.image.tag=${{ github.sha }} 59 | - name: Create contacts configmap 60 | run: kubectl apply -f deployment/k8s/configmap.yaml --namespace project-llllm 61 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # streamlit cache 163 | cache/ 164 | 165 | # AIM experiment runs 166 | .aim/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mambaorg/micromamba:1.4-bullseye-slim 2 | 3 | USER root 4 | RUN apt update && apt install -y gcc python3-dev \ 5 | && rm -rf /var/lib/apt/lists/* 6 | USER $MAMBA_USER 7 | 8 | COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yaml /tmp/env.yaml 9 | # TODO: avoid installing development dependencies 10 | RUN micromamba env create --yes -f /tmp/env.yaml && \ 11 | micromamba clean --all --yes 12 | 13 | EXPOSE 8501 14 | 15 | COPY . /app 16 | WORKDIR /app 17 | 18 | HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health 19 | 20 | ENTRYPOINT ["/opt/conda/envs/llllm-env/bin/streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Development Seed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # llllm 2 | 3 | A suite of tools to perform geospatial operations using Large Language Models. 4 | 5 | LLLLM stands for Lat-Lng-Large-Language-Model, you can call it as "el el el el emm" or "L4M". 6 | 7 | ## Setup 8 | 1. Create the llllm-env - `mamba env create -f environment.yaml` 9 | 2. Set your OpenAI API key as an environment variable - `export OPENAI_API_KEY=` 10 | 11 | ## Getting Started 12 | 13 | ### Adding a new tool 14 | 15 | Tools are ways the agent can use to interact with the outside world. You can find more information on how LLMs use tools to solve new tasks at scale in this paper: [Toolformer](https://arxiv.org/pdf/2302.04761.pdf) 16 | 17 | Langchain comes bundled with a set of tools like Google Search, Wikipedia, Python REPL, Shell, Wolfram Alpha & several others - find the list of pre-built tools [here](https://python.langchain.com/en/latest/modules/agents/tools.html#) 18 | 19 | Creating a new tool is simple in Langchain. You can create a custom tool by: 20 | - using the `Tool` dataclass or 21 | - inhering from the `BaseTool` class 22 | 23 | Both these methods need to provide: 24 | - name: unique & referred to by LLM while executing the chains 25 | - description: detailed description of when & how the LLM should use this tool 26 | - args_schema: arguments to the tool 27 | 28 | Barebones of how to define a custom tool by inheriting from the `BaseTool` class 29 | ```python 30 | from langchain.tools import BaseTool 31 | 32 | class YourCustomTool(BaseTool): 33 | name = 34 | description = 35 | args_schema = 36 | 37 | def _run(self, query): 38 | # functionality of the tool 39 | pass 40 | 41 | def _arun(self, query): 42 | # async implementation of the tool (Optional) 43 | raise NotImplementedError 44 | ``` 45 | We have a few tools available in the LLLLM toolkit that you can use for reference - [MercantileTool](tools/mercantile_tool.py), [GeoPyTool](tools/geopy/), [OSMnxTool](tools/osmnx/) 46 | 47 | Learn more about creating custom tools by reading this blog on [Building Custom Tools for LLM Agents from Pinecone](https://www.pinecone.io/learn/langchain-tools/) or [documention from Langchain](https://python.langchain.com/en/latest/modules/agents/tools/custom_tools.html). 48 | 49 | 50 | ### Creating an agent 51 | 52 | Agents act like routers using LLMs to decide which tools to use for the task at hand. There are different types of agents available in langchain: 53 | - [ReAct](https://python.langchain.com/en/latest/modules/agents/agents/examples/react.html) - Reason & Act agents are optimized for picking tools for best response - read more about them in this [paper](https://react-lm.github.io/) 54 | - [Conversation Agent](https://python.langchain.com/en/latest/modules/agents/agents/examples/chat_conversation_agent.html) - they are ideal to use in a conversational setting 55 | - [Structured Tool Chat Agent](https://python.langchain.com/en/latest/modules/agents/agents/examples/structured_chat.html) - use this agent if you have tools that expect multi-input parameters. Check the OSMnx or GeoPy tool for reference. 56 | 57 | Usage 58 | ```python 59 | from agents.l4m_agent import base_agent 60 | 61 | agent = base_agent( 62 | llm=, # instance of an LLM like GPT-3.5-TURBO or GPT-4 63 | tools=[, , ...], # list of tools the agent has access to perform its tasks 64 | name=, # Agent.Type for eg: zero-shot-react-description or structure-chat-zero-shot-react-description (for multi-input tools) 65 | ) 66 | ``` 67 | 68 | ### Creating notebooks to test 69 | Import the tools & create an agent in jupyter notebook to interact with them. Please find example notebooks [here](/nbs/). 70 | 71 | 72 | ### Running the streamlit app 73 | `streamlit run app.py` 74 | -------------------------------------------------------------------------------- /agents/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/llllm/40d2d73d063a72c52babb56dfa5343b7d41dc8ac/agents/.gitkeep -------------------------------------------------------------------------------- /agents/l4m_agent.py: -------------------------------------------------------------------------------- 1 | from langchain.agents import initialize_agent 2 | from langchain.agents import AgentType 3 | from langchain.prompts import MessagesPlaceholder 4 | from langchain.memory import ConversationBufferMemory 5 | 6 | 7 | def base_agent( 8 | llm, tools, agent_type=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION 9 | ): 10 | """Base agent to perform xyz slippy map tiles operations. 11 | 12 | llm: LLM object 13 | tools: List of tools to use by the agent 14 | """ 15 | # chat_history = MessagesPlaceholder(variable_name="chat_history") 16 | # memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) 17 | agent = initialize_agent( 18 | llm=llm, 19 | tools=tools, 20 | agent=agent_type, 21 | max_iterations=5, 22 | early_stopping_method="generate", 23 | verbose=True, 24 | # memory=memory, 25 | # agent_kwargs={ 26 | # "memory_prompts": [chat_history], 27 | # "input_variables": ["input", "agent_scratchpad", "chat_history"], 28 | # }, 29 | ) 30 | print("agent initialized") 31 | return agent 32 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import rasterio as rio 4 | import folium 5 | import streamlit as st 6 | from streamlit_folium import folium_static 7 | 8 | import langchain 9 | from langchain.agents import AgentType 10 | from langchain.chat_models import ChatOpenAI 11 | from langchain.tools import Tool, DuckDuckGoSearchRun 12 | from langchain.callbacks import ( 13 | StreamlitCallbackHandler, 14 | AimCallbackHandler, 15 | get_openai_callback, 16 | ) 17 | 18 | from tools.mercantile_tool import MercantileTool 19 | from tools.geopy.geocode import GeopyGeocodeTool 20 | from tools.geopy.distance import GeopyDistanceTool 21 | from tools.osmnx.geometry import OSMnxGeometryTool 22 | from tools.osmnx.network import OSMnxNetworkTool 23 | from tools.stac.search import STACSearchTool 24 | from agents.l4m_agent import base_agent 25 | 26 | # DEBUG 27 | langchain.debug = True 28 | 29 | 30 | @st.cache_resource(ttl="1h") 31 | def get_agent( 32 | openai_api_key, agent_type=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION 33 | ): 34 | llm = ChatOpenAI( 35 | temperature=0, 36 | openai_api_key=openai_api_key, 37 | model_name="gpt-3.5-turbo-0613", 38 | ) 39 | # define a set of tools the agent has access to for queries 40 | duckduckgo_tool = Tool( 41 | name="DuckDuckGo", 42 | description="Use this tool to answer questions about current events and places. \ 43 | Please ask targeted questions.", 44 | func=DuckDuckGoSearchRun().run, 45 | ) 46 | geocode_tool = GeopyGeocodeTool() 47 | distance_tool = GeopyDistanceTool() 48 | mercantile_tool = MercantileTool() 49 | geometry_tool = OSMnxGeometryTool() 50 | network_tool = OSMnxNetworkTool() 51 | search_tool = STACSearchTool() 52 | 53 | tools = [ 54 | duckduckgo_tool, 55 | geocode_tool, 56 | distance_tool, 57 | mercantile_tool, 58 | geometry_tool, 59 | network_tool, 60 | search_tool, 61 | ] 62 | 63 | agent = base_agent(llm, tools, agent_type=agent_type) 64 | return agent 65 | 66 | 67 | def run_query(agent, query): 68 | return response 69 | 70 | 71 | def plot_raster(items): 72 | st.subheader("Preview of the first item sorted by cloud cover") 73 | selected_item = min(items, key=lambda item: item.properties["eo:cloud_cover"]) 74 | href = selected_item.assets["rendered_preview"].href 75 | # arr = rio.open(href).read() 76 | 77 | # m = folium.Map(location=[28.6, 77.7], zoom_start=6) 78 | 79 | # img = folium.raster_layers.ImageOverlay( 80 | # name="Sentinel 2", 81 | # image=arr.transpose(1, 2, 0), 82 | # bounds=selected_item.bbox, 83 | # opacity=0.9, 84 | # interactive=True, 85 | # cross_origin=False, 86 | # zindex=1, 87 | # ) 88 | 89 | # img.add_to(m) 90 | # folium.LayerControl().add_to(m) 91 | 92 | # folium_static(m) 93 | st.image(href) 94 | 95 | 96 | def plot_vector(df): 97 | st.subheader("Add the geometry to the Map") 98 | center = df.centroid.iloc[0] 99 | m = folium.Map(location=[center.y, center.x], zoom_start=12) 100 | folium.GeoJson(df).add_to(m) 101 | folium_static(m) 102 | 103 | 104 | st.set_page_config(page_title="LLLLM", page_icon="🤖", layout="wide") 105 | st.subheader("🤖 I am Geo LLM Agent!") 106 | 107 | if "msgs" not in st.session_state: 108 | st.session_state.msgs = [] 109 | 110 | if "total_tokens" not in st.session_state: 111 | st.session_state.total_tokens = 0 112 | 113 | if "prompt_tokens" not in st.session_state: 114 | st.session_state.prompt_tokens = 0 115 | 116 | if "completion_tokens" not in st.session_state: 117 | st.session_state.completion_tokens = 0 118 | 119 | if "total_cost" not in st.session_state: 120 | st.session_state.total_cost = 0 121 | 122 | with st.sidebar: 123 | openai_api_key = os.getenv("OPENAI_API_KEY") 124 | if not openai_api_key: 125 | openai_api_key = st.text_input("OpenAI API Key", type="password") 126 | 127 | st.subheader("OpenAI Usage") 128 | total_tokens = st.empty() 129 | prompt_tokens = st.empty() 130 | completion_tokens = st.empty() 131 | total_cost = st.empty() 132 | 133 | total_tokens.write(f"Total Tokens: {st.session_state.total_tokens:,.0f}") 134 | prompt_tokens.write(f"Prompt Tokens: {st.session_state.prompt_tokens:,.0f}") 135 | completion_tokens.write( 136 | f"Completion Tokens: {st.session_state.completion_tokens:,.0f}" 137 | ) 138 | total_cost.write(f"Total Cost (USD): ${st.session_state.total_cost:,.4f}") 139 | 140 | 141 | for msg in st.session_state.msgs: 142 | with st.chat_message(name=msg["role"], avatar=msg["avatar"]): 143 | st.markdown(msg["content"]) 144 | 145 | if prompt := st.chat_input("Ask me anything about the flat world..."): 146 | with st.chat_message(name="user", avatar="🧑‍💻"): 147 | st.markdown(prompt) 148 | 149 | st.session_state.msgs.append({"role": "user", "avatar": "🧑‍💻", "content": prompt}) 150 | 151 | if not openai_api_key: 152 | st.info("Please add your OpenAI API key to continue.") 153 | st.stop() 154 | 155 | aim_callback = AimCallbackHandler( 156 | repo=".", 157 | experiment_name="LLLLLM: Base Agent v0.1", 158 | ) 159 | 160 | agent = get_agent(openai_api_key) 161 | 162 | with get_openai_callback() as cb: 163 | st_callback = StreamlitCallbackHandler(st.container()) 164 | response = agent.run(prompt, callbacks=[st_callback, aim_callback]) 165 | 166 | aim_callback.flush_tracker(langchain_asset=agent, reset=False, finish=True) 167 | 168 | # Log OpenAI stats 169 | # print(f"Model name: {response.llm_output.get('model_name', '')}") 170 | st.session_state.total_tokens += cb.total_tokens 171 | st.session_state.prompt_tokens += cb.prompt_tokens 172 | st.session_state.completion_tokens += cb.completion_tokens 173 | st.session_state.total_cost += cb.total_cost 174 | 175 | total_tokens.write(f"Total Tokens: {st.session_state.total_tokens:,.0f}") 176 | prompt_tokens.write(f"Prompt Tokens: {st.session_state.prompt_tokens:,.0f}") 177 | completion_tokens.write( 178 | f"Completion Tokens: {st.session_state.completion_tokens:,.0f}" 179 | ) 180 | total_cost.write(f"Total Cost (USD): ${st.session_state.total_cost:,.4f}") 181 | 182 | with st.chat_message(name="assistant", avatar="🤖"): 183 | if type(response) == str: 184 | content = response 185 | st.markdown(response) 186 | else: 187 | tool, result = response 188 | 189 | match tool: 190 | case "stac-search": 191 | content = f"Found {len(result)} items from the catalog." 192 | st.markdown(content) 193 | if len(result) > 0: 194 | plot_raster(result) 195 | case "geometry": 196 | content = f"Found {len(result)} geometries." 197 | gdf = result 198 | st.markdown(content) 199 | plot_vector(gdf) 200 | case "network": 201 | content = f"Found {len(result)} network geometries." 202 | ndf = result 203 | st.markdown(content) 204 | plot_vector(ndf) 205 | case _: 206 | content = response 207 | st.markdown(content) 208 | 209 | st.session_state.msgs.append( 210 | {"role": "assistant", "avatar": "🤖", "content": content} 211 | ) 212 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deployment workflow 2 | 3 | The streamlit app is deployed to the Labs GKE cluster through [Helm](https://helm.sh/). The Helm chart is located in the [`helm`](./helm/) directory. 4 | 5 | The deployment workflow is defined in the [`.github/workflows/build_and_deploy.yaml`](../.github/workflows/build_and_deploy.yaml) file. It is triggered on push to the `main` branch. First the docker image is built and pushed to Github Container Registry. Then, the Helm chart is deployed to the GKE cluster. 6 | 7 | The [`llllm-contacts` ConfigMap](./k8s/configmap.yaml) is used to store the contact details of the team members responsible for the app. 8 | -------------------------------------------------------------------------------- /deployment/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: llllm 3 | description: Helm chart for llllm streamlit app 4 | type: application 5 | version: 0.0.1 6 | appVersion: 0.0.1 7 | -------------------------------------------------------------------------------- /deployment/helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: llllm-ingress 5 | annotations: 6 | kubernetes.io/ingress.class: "nginx" 7 | cert-manager.io/issuer: "letsencrypt-prod" 8 | spec: 9 | tls: 10 | - hosts: 11 | - "{{ .Values.streamlit.host }}" 12 | secretName: llllm-tls 13 | rules: 14 | - host: "{{ .Values.streamlit.host }}" 15 | http: 16 | paths: 17 | - backend: 18 | service: 19 | name: llllm-streamlit-app-service 20 | port: 21 | number: 8501 22 | path: / 23 | pathType: Prefix 24 | -------------------------------------------------------------------------------- /deployment/helm/templates/letsencrypt-cert-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Issuer 3 | metadata: 4 | name: letsencrypt-staging 5 | spec: 6 | acme: 7 | # The ACME server URL 8 | server: https://acme-staging-v02.api.letsencrypt.org/directory 9 | # Email address used for ACME registration 10 | email: tarashish@developmentseed.org 11 | # Name of a secret used to store the ACME account private key 12 | privateKeySecretRef: 13 | name: letsencrypt-staging-key 14 | # Enable the HTTP-01 challenge provider 15 | solvers: 16 | - http01: 17 | ingress: 18 | class: nginx 19 | --- 20 | apiVersion: cert-manager.io/v1 21 | kind: Issuer 22 | metadata: 23 | name: letsencrypt-prod 24 | spec: 25 | acme: 26 | # The ACME server URL 27 | server: https://acme-v02.api.letsencrypt.org/directory 28 | # Email address used for ACME registration 29 | email: tarashish@developmentseed.org 30 | # Name of a secret used to store the ACME account private key 31 | privateKeySecretRef: 32 | name: letsencrypt-prod-key 33 | # Enable the HTTP-01 challenge provider 34 | solvers: 35 | - http01: 36 | ingress: 37 | class: nginx -------------------------------------------------------------------------------- /deployment/helm/templates/secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: llllm-secrets 5 | type: Opaque 6 | data: 7 | openai_api_key: {{ .Values.secrets.openai_api_key | b64enc }} 8 | -------------------------------------------------------------------------------- /deployment/helm/templates/streamlit.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: llllm-streamlit-app 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: llllm-streamlit-app 10 | template: 11 | metadata: 12 | labels: 13 | app: llllm-streamlit-app 14 | spec: 15 | containers: 16 | - name: llllm-streamlit-app 17 | image: {{ .Values.streamlit.image.repository }}:{{ .Values.streamlit.image.tag }} 18 | command: ["/opt/conda/envs/llllm-env/bin/streamlit"] 19 | args: ["run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] 20 | env: 21 | - name: OPENAI_API_KEY 22 | valueFrom: 23 | secretKeyRef: 24 | name: llllm-secrets 25 | key: openai_api_key 26 | ports: 27 | - containerPort: 8501 28 | volumeMounts: 29 | - name: cache-volume 30 | mountPath: /app/cache 31 | volumes: 32 | - name: cache-volume 33 | emptyDir: {} 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: llllm-streamlit-app-service 39 | spec: 40 | selector: 41 | app: llllm-streamlit-app 42 | ports: 43 | - protocol: TCP 44 | port: 8501 45 | targetPort: 8501 46 | type: ClusterIP 47 | -------------------------------------------------------------------------------- /deployment/helm/values.yaml: -------------------------------------------------------------------------------- 1 | streamlit: 2 | image: 3 | repository: ghcr.io/developmentseed/llllm 4 | tag: latest 5 | host: llllm.k8s.labs.ds.io 6 | 7 | secrets: 8 | openai_api_key: 9 | -------------------------------------------------------------------------------- /deployment/k8s/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: llllm-contacts 5 | data: 6 | project: llllm 7 | repo: "github.com/developmentseed/llllm" 8 | contacts: | 9 | - name: Soumya Ranjan Mohanty 10 | email: soumya@developmentseed.org 11 | slack: srm 12 | -------------------------------------------------------------------------------- /environment.yaml: -------------------------------------------------------------------------------- 1 | name: llllm-env 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3 6 | - pip 7 | - osmnx=1.3.1 8 | - pip: 9 | - openai==0.27.8 10 | - langchain==0.0.215 11 | - duckduckgo-search==3.8.3 12 | - mercantile==1.2.1 13 | - geopy==2.3.0 14 | - ipywidgets==8.0.6 15 | - jupyterlab==4.0.2 16 | - planetary-computer==0.5.1 17 | - pystac-client==0.7.2 18 | - streamlit==1.24.1 19 | - streamlit-folium==0.12.0 20 | - watchdog==3.0.0 21 | - aim==3.17.5 22 | -------------------------------------------------------------------------------- /nbs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/llllm/40d2d73d063a72c52babb56dfa5343b7d41dc8ac/nbs/.gitkeep -------------------------------------------------------------------------------- /nbs/23-05-18_mercantile-tool.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "699f101a-66bb-4ae7-9ed6-01a27d028266", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import sys\n", 11 | "sys.path.append(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "58134048-4a6e-4ab9-87ee-04947085c30e", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import os\n", 22 | "\n", 23 | "from langchain.chat_models import ChatOpenAI\n", 24 | "from langchain.tools import BaseTool, DuckDuckGoSearchRun\n", 25 | "from langchain.agents import Tool\n", 26 | "\n", 27 | "from tools.mercantile_tool import MercantileTool\n", 28 | "from agents.l4m_agent import base_agent" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "id": "3909647e-af00-4489-b073-4b792e250af2", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 4, 44 | "id": "3c032737-e0e4-4b7a-9f6a-3b722fc0e032", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "# pick a LLM\n", 49 | "llm = ChatOpenAI(\n", 50 | " temperature=0,\n", 51 | " openai_api_key=OPENAI_API_KEY,\n", 52 | " model_name=\"gpt-3.5-turbo\"\n", 53 | ")" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 5, 59 | "id": "660da8b8-f422-4f31-a3aa-15296709033f", 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# define a set of tools the agent has access to for queries\n", 64 | "duckduckgo_tool = Tool(\n", 65 | " name=\"DuckDuckGo\",\n", 66 | " description=\"Use this tool to answer questions about current events and places. \\\n", 67 | " Please ask targeted questions.\",\n", 68 | " func=DuckDuckGoSearchRun().run\n", 69 | ")\n", 70 | "\n", 71 | "mercantile_tool = MercantileTool()\n", 72 | "\n", 73 | "tools = [duckduckgo_tool, mercantile_tool]" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 6, 79 | "id": "b4c4291d-35c9-48b7-8b45-7da5ff155db7", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "agent = base_agent(llm, tools) " 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 8, 89 | "id": "6baab55c-892f-4938-8f18-c2b8c8712cf2", 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "name": "stdout", 94 | "output_type": "stream", 95 | "text": [ 96 | "\n", 97 | "\n", 98 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 99 | "\u001b[32;1m\u001b[1;3mI need to use mercantile to get the xyz tile for London, but I need to find the longitude and latitude coordinates for London first.\n", 100 | "Action: DuckDuckGo\n", 101 | "Action Input: \"London longitude and latitude\"\u001b[0m\n", 102 | "Observation: \u001b[36;1m\u001b[1;3mLondon, city, capital of the United Kingdom. It is among the oldest of the world's great cities—its history spanning nearly two millennia—and one of the most cosmopolitan. By far Britain's largest metropolis, it is also the country's economic, transportation, and cultural centre. London is situated in southeastern England, lying astride the River Thames some 50 miles (80 km) upstream ... The length of a degree of arc of latitude is approximately 111 km (69 miles), varying, because of the nonuniformity of Earth's curvature, from 110.567 km (68.706 miles) at the Equator to 111.699 km (69.41 miles) at the poles. Geographic latitude is also given in degrees, minutes, and seconds. facts about lines of longitude London (51°30′N) is farther north than Calgary (51°03′N) with Amsterdam, Berlin and Dublin being located even further north. Montreal is south of Paris. Phoenix is placed close to the Ancient city of Carthage, which was a Phoenician city-state. People from Phoenix today are known as Phoenicians. (Credit: reddit users twomancanoe and svaachkuet) Covering an area of 130,279 sq. km, England is the largest country in the United Kingdom. Located in the southeastern part of the country, along the banks of the Thames River is London - the capital and the largest city of England. London serves as one of the most important global cities in the world. Where is England? Greenwich Mean Time or GMT is mean (average) solar time at the Greenwich Meridian or Prime Meridian, 0 degrees longitude. The Prime Meridian is the reference point for every time zone in the world. The time displayed by the Shepherd Gate Clock at the Royal Observatory in Greenwich, London, is always GMT. When the sun is at its highest point ...\u001b[0m\n", 103 | "Thought:\u001b[32;1m\u001b[1;3mNow that I have the longitude and latitude information for London, I can use mercantile to get the xyz tile.\n", 104 | "Action: mercantile\n", 105 | "Action Input: \"-0.1278, 51.5074, 10\"\u001b[0m\n", 106 | "Observation: \u001b[33;1m\u001b[1;3mTile(x=511, y=340, z=10.0)\u001b[0m\n", 107 | "Thought:\u001b[32;1m\u001b[1;3mThe xyz tile for London is Tile(x=511, y=340, z=10.0)\n", 108 | "Final Answer: Tile(x=511, y=340, z=10.0)\u001b[0m\n", 109 | "\n", 110 | "\u001b[1m> Finished chain.\u001b[0m\n" 111 | ] 112 | }, 113 | { 114 | "data": { 115 | "text/plain": [ 116 | "{'input': 'What is the xyz tile for London?',\n", 117 | " 'output': 'Tile(x=511, y=340, z=10.0)'}" 118 | ] 119 | }, 120 | "execution_count": 8, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "agent(\"What is the xyz tile for London?\")" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "id": "7527007d-35c5-4a53-91ac-4b71acf36ee3", 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [] 136 | } 137 | ], 138 | "metadata": { 139 | "kernelspec": { 140 | "display_name": "Python 3 (ipykernel)", 141 | "language": "python", 142 | "name": "python3" 143 | }, 144 | "language_info": { 145 | "codemirror_mode": { 146 | "name": "ipython", 147 | "version": 3 148 | }, 149 | "file_extension": ".py", 150 | "mimetype": "text/x-python", 151 | "name": "python", 152 | "nbconvert_exporter": "python", 153 | "pygments_lexer": "ipython3", 154 | "version": "3.11.3" 155 | } 156 | }, 157 | "nbformat": 4, 158 | "nbformat_minor": 5 159 | } 160 | -------------------------------------------------------------------------------- /nbs/23-05-19_geopy-tool.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "93c3087f-de40-4589-aab9-8d316cfc4894", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import sys\n", 11 | "sys.path.append(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "78736805-496c-46ff-8249-5cdd220dfd18", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import os\n", 22 | "\n", 23 | "from langchain.chat_models import ChatOpenAI\n", 24 | "from langchain.tools import BaseTool, DuckDuckGoSearchRun\n", 25 | "from langchain.agents import Tool\n", 26 | "\n", 27 | "from tools.mercantile_tool import MercantileTool\n", 28 | "from tools.geopy.geocode import GeopyGeocodeTool\n", 29 | "from tools.geopy.distance import GeopyDistanceTool\n", 30 | "from agents.l4m_agent import base_agent" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 3, 36 | "id": "8416cf6b-54d7-4e55-a8f4-1e7c3ef550ee", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 4, 46 | "id": "01a127d8-7941-4adf-bb2e-24bd67eb0947", 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "# pick a LLM\n", 51 | "llm = ChatOpenAI(\n", 52 | " temperature=0,\n", 53 | " openai_api_key=OPENAI_API_KEY,\n", 54 | " model_name=\"gpt-3.5-turbo\"\n", 55 | ")" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 5, 61 | "id": "f4f97119-4926-44ce-9f95-21121a83860a", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# define a set of tools the agent has access to for queries\n", 66 | "duckduckgo_tool = Tool(\n", 67 | " name=\"DuckDuckGo\",\n", 68 | " description=\"Use this tool to answer questions about current events and places. \\\n", 69 | " Please ask targeted questions.\",\n", 70 | " func=DuckDuckGoSearchRun().run\n", 71 | ")\n", 72 | "\n", 73 | "geocode_tool = GeopyGeocodeTool()\n", 74 | "distance_tool = GeopyDistanceTool()\n", 75 | "mercantile_tool = MercantileTool()\n", 76 | "\n", 77 | "tools = [geocode_tool, distance_tool, mercantile_tool]" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 6, 83 | "id": "b84fb2cf-e63d-49b0-b6f3-bc8116c51491", 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "agent = base_agent(llm, tools, name=\"structured-chat-zero-shot-react-description\")" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 7, 93 | "id": "fa3fdc6d-5b98-423f-ae6c-a0faf6405dae", 94 | "metadata": {}, 95 | "outputs": [ 96 | { 97 | "name": "stdout", 98 | "output_type": "stream", 99 | "text": [ 100 | "Respond to the human as helpfully and accurately as possible. You have access to the following tools:\n", 101 | "\n", 102 | "geocode: Use this tool for geocoding., args: {{{{'place': {{{{'title': 'Place', 'description': 'name of a place', 'type': 'string'}}}}}}}}\n", 103 | "distance: Use this tool to compute distance between two points available in lat,lng format., args: {{{{'point_1': {{{{'title': 'Point 1', 'description': 'lat,lng of a place', 'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{{{{'type': 'number'}}}}, {{{{'type': 'number'}}}}]}}}}, 'point_2': {{{{'title': 'Point 2', 'description': 'lat,lng of a place', 'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{{{{'type': 'number'}}}}, {{{{'type': 'number'}}}}]}}}}}}}}\n", 104 | "mercantile: Use this tool to get the xyz tiles given a lng,lat coordinate. To use this tool you need to provide lng,lat,zoom level separated by comma. Eg: `-105.24, 22.50, 5` is the input to get a tile for this (lng=-105.24, lat=22.50) at zoom level 5, args: {{{{'query': {{{{'title': 'Query'}}}}}}}}\n", 105 | "\n", 106 | "Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).\n", 107 | "\n", 108 | "Valid \"action\" values: \"Final Answer\" or geocode, distance, mercantile\n", 109 | "\n", 110 | "Provide only ONE action per $JSON_BLOB, as shown:\n", 111 | "\n", 112 | "```\n", 113 | "{{\n", 114 | " \"action\": $TOOL_NAME,\n", 115 | " \"action_input\": $INPUT\n", 116 | "}}\n", 117 | "```\n", 118 | "\n", 119 | "Follow this format:\n", 120 | "\n", 121 | "Question: input question to answer\n", 122 | "Thought: consider previous and subsequent steps\n", 123 | "Action:\n", 124 | "```\n", 125 | "$JSON_BLOB\n", 126 | "```\n", 127 | "Observation: action result\n", 128 | "... (repeat Thought/Action/Observation N times)\n", 129 | "Thought: I know what to respond\n", 130 | "Action:\n", 131 | "```\n", 132 | "{{\n", 133 | " \"action\": \"Final Answer\",\n", 134 | " \"action_input\": \"Final response to human\"\n", 135 | "}}\n", 136 | "```\n", 137 | "\n", 138 | "Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation:.\n", 139 | "Thought:\n" 140 | ] 141 | } 142 | ], 143 | "source": [ 144 | "print(agent.agent.llm_chain.prompt.messages[0].prompt.template)" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 8, 150 | "id": "36cfa0c3-643f-4a12-a505-67574cbd1554", 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "name": "stdout", 155 | "output_type": "stream", 156 | "text": [ 157 | "\n", 158 | "\n", 159 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 160 | "\u001b[32;1m\u001b[1;3mAction:\n", 161 | "```\n", 162 | "{\n", 163 | " \"action\": \"distance\",\n", 164 | " \"action_input\": {\n", 165 | " \"point_1\": [51.5074, 0.1278],\n", 166 | " \"point_2\": [48.8566, 2.3522]\n", 167 | " }\n", 168 | "}\n", 169 | "```\n", 170 | "\u001b[0m\n", 171 | "Observation: \u001b[33;1m\u001b[1;3m334.89654742728425\u001b[0m\n", 172 | "Thought:\u001b[32;1m\u001b[1;3mThe distance between London and Paris is 334.9 km.\n", 173 | "Action:\n", 174 | "```\n", 175 | "{\n", 176 | " \"action\": \"Final Answer\",\n", 177 | " \"action_input\": \"The distance between London and Paris is 334.9 km.\"\n", 178 | "}\n", 179 | "```\n", 180 | "\u001b[0m\n", 181 | "\n", 182 | "\u001b[1m> Finished chain.\u001b[0m\n" 183 | ] 184 | }, 185 | { 186 | "data": { 187 | "text/plain": [ 188 | "{'input': 'What is the distance between London to Paris?',\n", 189 | " 'output': 'The distance between London and Paris is 334.9 km.'}" 190 | ] 191 | }, 192 | "execution_count": 8, 193 | "metadata": {}, 194 | "output_type": "execute_result" 195 | } 196 | ], 197 | "source": [ 198 | "agent(\"What is the distance between London to Paris?\")" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 9, 204 | "id": "ae767f5e", 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "name": "stdout", 209 | "output_type": "stream", 210 | "text": [ 211 | "\n", 212 | "\n", 213 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 214 | "\u001b[32;1m\u001b[1;3mAction:\n", 215 | "```\n", 216 | "{\n", 217 | " \"action\": \"geocode\",\n", 218 | " \"action_input\": {\n", 219 | " \"place\": \"London\"\n", 220 | " }\n", 221 | "}\n", 222 | "```\n", 223 | "\u001b[0m\n", 224 | "Observation: \u001b[36;1m\u001b[1;3m(51.5073359, -0.12765)\u001b[0m\n", 225 | "Thought:\u001b[32;1m\u001b[1;3mWhat is the distance between London and Paris?\n", 226 | "\n", 227 | "Action:\n", 228 | "```\n", 229 | "{\n", 230 | " \"action\": \"distance\",\n", 231 | " \"action_input\": {\n", 232 | " \"point_1\": [51.5073359, -0.12765],\n", 233 | " \"point_2\": [48.8566969, 2.3514616]\n", 234 | " }\n", 235 | "}\n", 236 | "```\n", 237 | "\n", 238 | "\u001b[0m\n", 239 | "Observation: \u001b[33;1m\u001b[1;3m343.8751004027932\u001b[0m\n", 240 | "Thought:\u001b[32;1m\u001b[1;3mWhat is the xyz tile for the coordinates 51.5073359, -0.12765 at zoom level 10?\n", 241 | "\n", 242 | "Action:\n", 243 | "```\n", 244 | "{\n", 245 | " \"action\": \"mercantile\",\n", 246 | " \"action_input\": {\n", 247 | " \"query\": \"51.5073359, -0.12765, 10\"\n", 248 | " }\n", 249 | "}\n", 250 | "```\n", 251 | "\n", 252 | "\u001b[0m\n", 253 | "Observation: \u001b[38;5;200m\u001b[1;3mTile(x=658, y=512, z=10.0)\u001b[0m\n", 254 | "Thought:\u001b[32;1m\u001b[1;3mAction:\n", 255 | "```\n", 256 | "{\n", 257 | " \"action\": \"Final Answer\",\n", 258 | " \"action_input\": \"The distance between London and Paris is approximately 343.88 km and the xyz tile for the coordinates 51.5073359, -0.12765 at zoom level 10 is Tile(x=658, y=512, z=10.0).\"\n", 259 | "}\n", 260 | "```\n", 261 | "\n", 262 | "\u001b[0m\n", 263 | "\n", 264 | "\u001b[1m> Finished chain.\u001b[0m\n" 265 | ] 266 | }, 267 | { 268 | "data": { 269 | "text/plain": [ 270 | "{'input': 'What is lat,lng of London?',\n", 271 | " 'output': 'The distance between London and Paris is approximately 343.88 km and the xyz tile for the coordinates 51.5073359, -0.12765 at zoom level 10 is Tile(x=658, y=512, z=10.0).'}" 272 | ] 273 | }, 274 | "execution_count": 9, 275 | "metadata": {}, 276 | "output_type": "execute_result" 277 | } 278 | ], 279 | "source": [ 280 | "agent(\"What is lat,lng of London?\")" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 10, 286 | "id": "94eee070-2718-4ce8-922e-44b0a45b2801", 287 | "metadata": {}, 288 | "outputs": [ 289 | { 290 | "name": "stdout", 291 | "output_type": "stream", 292 | "text": [ 293 | "\n", 294 | "\n", 295 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 296 | "\u001b[32;1m\u001b[1;3mThought: I can use the `geocode` tool to get the latitude and longitude of London, and then use the `mercantile` tool to get the XYZ tile covering it.\n", 297 | "\n", 298 | "Action:\n", 299 | "```\n", 300 | "{\n", 301 | " \"action\": \"mercantile\",\n", 302 | " \"action_input\": {\n", 303 | " \"query\": \"51.5074, 0.1278, 10\"\n", 304 | " }\n", 305 | "}\n", 306 | "```\n", 307 | "\n", 308 | "\u001b[0m\n", 309 | "Observation: \u001b[38;5;200m\u001b[1;3mTile(x=658, y=511, z=10.0)\u001b[0m\n", 310 | "Thought:\u001b[32;1m\u001b[1;3mThe XYZ tile covering London is Tile(x=658, y=511, z=10.0).\n", 311 | "Action:\n", 312 | "```\n", 313 | "{\n", 314 | " \"action\": \"Final Answer\",\n", 315 | " \"action_input\": \"The XYZ tile covering London is Tile(x=658, y=511, z=10.0).\"\n", 316 | "}\n", 317 | "```\n", 318 | "\n", 319 | "\n", 320 | "\u001b[0m\n", 321 | "\n", 322 | "\u001b[1m> Finished chain.\u001b[0m\n" 323 | ] 324 | }, 325 | { 326 | "data": { 327 | "text/plain": [ 328 | "{'input': 'What is the XYZ tile covering London?',\n", 329 | " 'output': 'The XYZ tile covering London is Tile(x=658, y=511, z=10.0).'}" 330 | ] 331 | }, 332 | "execution_count": 10, 333 | "metadata": {}, 334 | "output_type": "execute_result" 335 | } 336 | ], 337 | "source": [ 338 | "agent(\"What is the XYZ tile covering London?\")" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": null, 344 | "id": "5c0c6537-977d-4e1d-8dc1-5fc68b655d25", 345 | "metadata": {}, 346 | "outputs": [], 347 | "source": [] 348 | } 349 | ], 350 | "metadata": { 351 | "kernelspec": { 352 | "display_name": "Python 3 (ipykernel)", 353 | "language": "python", 354 | "name": "python3" 355 | }, 356 | "language_info": { 357 | "codemirror_mode": { 358 | "name": "ipython", 359 | "version": 3 360 | }, 361 | "file_extension": ".py", 362 | "mimetype": "text/x-python", 363 | "name": "python", 364 | "nbconvert_exporter": "python", 365 | "pygments_lexer": "ipython3", 366 | "version": "3.11.3" 367 | } 368 | }, 369 | "nbformat": 4, 370 | "nbformat_minor": 5 371 | } 372 | -------------------------------------------------------------------------------- /nbs/23-05-26_osmnx-tool.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "9ec30f55-baa1-486c-9277-8e80e262edeb", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import sys\n", 11 | "sys.path.append(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "0321b936-0620-44d9-ab94-5afe2cc02baa", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import os\n", 22 | "\n", 23 | "import osmnx as ox\n", 24 | "from osmnx import utils_graph\n", 25 | "import geopandas as gpd\n", 26 | "\n", 27 | "from langchain.chat_models import ChatOpenAI\n", 28 | "from langchain.tools import BaseTool, DuckDuckGoSearchRun\n", 29 | "from langchain.agents import Tool\n", 30 | "\n", 31 | "from tools.mercantile_tool import MercantileTool\n", 32 | "from tools.geopy.geocode import GeopyGeocodeTool\n", 33 | "from tools.geopy.distance import GeopyDistanceTool\n", 34 | "from agents.l4m_agent import base_agent\n", 35 | "\n", 36 | "from getpass import getpass" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 3, 42 | "id": "8152a613-7b88-4bec-b0a7-8b9256f3c763", 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "name": "stdin", 47 | "output_type": "stream", 48 | "text": [ 49 | " ········\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "OPENAI_API_KEY = getpass()" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 4, 60 | "id": "5eaa8573-86f2-4483-bcc9-7b9c82916f16", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 5, 70 | "id": "4c3fbc82-af1b-40a1-8edc-248c62f93fcd", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "from typing import Type, Dict\n", 75 | "\n", 76 | "from pydantic import BaseModel, Field\n", 77 | "from langchain.tools import BaseTool\n", 78 | "\n", 79 | "class PlaceWithTags(BaseModel):\n", 80 | " \"Name of a place and tags in OSM.\"\n", 81 | "\n", 82 | " place: str = Field(..., description=\"name of a place\")\n", 83 | " tags: Dict[str, str] = Field(..., description=\"open street maps tags\")\n", 84 | "\n", 85 | "\n", 86 | "class OSMnxGeometryTool(BaseTool):\n", 87 | " \"\"\"Custom tool to query geometries from OSM.\"\"\"\n", 88 | "\n", 89 | " name: str = \"geometry\"\n", 90 | " args_schema: Type[BaseModel] = PlaceWithTags\n", 91 | " description: str = \"Use this tool to get geometry of different features of a place like building footprints, parks, lakes, hospitals, schools etc. \\\n", 92 | " Pass the name of the place & relevant tags of Open Street Map as args.\"\n", 93 | " return_direct = True\n", 94 | "\n", 95 | " def _run(self, place: str, tags: Dict[str, str]) -> gpd.GeoDataFrame:\n", 96 | " gdf = ox.geometries_from_place(place, tags)\n", 97 | " gdf = gdf[gdf[\"geometry\"].type.isin({\"Polygon\", \"MultiPolygon\"})]\n", 98 | " gdf = gdf[[\"name\", \"geometry\"]].reset_index(drop=True).head(20)\n", 99 | " return gdf\n", 100 | "\n", 101 | " def _arun(self, place: str):\n", 102 | " raise NotImplementedError\n", 103 | "\n", 104 | "class PlaceWithNetworktype(BaseModel):\n", 105 | " \"Name of a place on the map\"\n", 106 | " place: str = Field(..., description=\"name of a place on the map\")\n", 107 | " network_type: str = Field(..., description=\"network type: one of walk, bike, drive or all\")\n", 108 | "\n", 109 | "class OSMnxNetworkTool(BaseTool):\n", 110 | " \"\"\"Custom tool to query road networks from OSM.\"\"\"\n", 111 | "\n", 112 | " name: str = \"network\"\n", 113 | " args_schema: Type[BaseModel] = PlaceWithNetworktype\n", 114 | " description: str = \"Use this tool to get road network of a place. \\\n", 115 | " Pass the name of the place & type of road network i.e walk, bike, drive or all.\"\n", 116 | " return_direct = True\n", 117 | "\n", 118 | " def _run(self, place: str, network_type: str) -> gpd.GeoDataFrame:\n", 119 | " G = ox.graph_from_place(place, network_type=network_type, simplify=True)\n", 120 | " network = utils_graph.graph_to_gdfs(G, nodes=False) \n", 121 | " network = network[[\"name\", \"geometry\"]].reset_index(drop=True).head(20)\n", 122 | " return network\n", 123 | " \n", 124 | " def _arun(self, place: str):\n", 125 | " raise NotImplementedError" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 6, 131 | "id": "9893d0e0-dd11-4fa2-82f7-da6a37ba83a0", 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "geometry_tool = OSMnxGeometryTool()\n", 136 | "network_tool = OSMnxNetworkTool()" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 7, 142 | "id": "153d4abf-4828-4ad3-970c-db47f0772547", 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "# pick a LLM\n", 147 | "llm = ChatOpenAI(\n", 148 | " temperature=0,\n", 149 | " openai_api_key=OPENAI_API_KEY,\n", 150 | " model_name=\"gpt-3.5-turbo\"\n", 151 | ")" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 8, 157 | "id": "473bf6d6-3489-42a3-ac12-6768b9c4f03b", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# define a set of tools the agent has access to for queries\n", 162 | "duckduckgo_tool = Tool(\n", 163 | " name=\"DuckDuckGo\",\n", 164 | " description=\"Use this tool to answer questions about current events and places. \\\n", 165 | " Please ask targeted questions.\",\n", 166 | " func=DuckDuckGoSearchRun().run\n", 167 | ")\n", 168 | "\n", 169 | "geocode_tool = GeopyGeocodeTool()\n", 170 | "distance_tool = GeopyDistanceTool()\n", 171 | "mercantile_tool = MercantileTool()\n", 172 | "\n", 173 | "tools = [duckduckgo_tool, network_tool, geocode_tool, distance_tool, mercantile_tool, geometry_tool]" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 9, 179 | "id": "ee95931c-b68b-4317-8c1d-227321e0c526", 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "agent = base_agent(llm, tools, name=\"structured-chat-zero-shot-react-description\")" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 10, 189 | "id": "3aaf6d47-157f-44e1-9aef-dda05d79c82a", 190 | "metadata": {}, 191 | "outputs": [ 192 | { 193 | "name": "stdout", 194 | "output_type": "stream", 195 | "text": [ 196 | "\n", 197 | "\n", 198 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 199 | "\u001b[32;1m\u001b[1;3mAction:\n", 200 | "```\n", 201 | "{\n", 202 | " \"action\": \"network\",\n", 203 | " \"action_input\": {\n", 204 | " \"place\": \"Bangalore, India\",\n", 205 | " \"network_type\": \"bike\"\n", 206 | " }\n", 207 | "}\n", 208 | "``` \n", 209 | "\u001b[0m\n", 210 | "Observation: \u001b[33;1m\u001b[1;3m name geometry\n", 211 | "0 2nd Main Road LINESTRING (77.59872 12.91054, 77.59848 12.91272)\n", 212 | "1 2nd Main Road LINESTRING (77.59872 12.91054, 77.59873 12.91049)\n", 213 | "2 9th Cross Road LINESTRING (77.59872 12.91054, 77.59899 12.91056)\n", 214 | "3 7th Cross Road LINESTRING (77.62408 12.93497, 77.62371 12.93526)\n", 215 | "4 Mahayogi Vemana Road LINESTRING (77.62408 12.93497, 77.62437 12.93524)\n", 216 | "5 3rd Main Ashwini Layout LINESTRING (77.62917 12.93849, 77.62887 12.93935)\n", 217 | "6 Inner Ring Road LINESTRING (77.62917 12.93849, 77.62938 12.938...\n", 218 | "7 Yelahanka Road LINESTRING (77.59420 13.09518, 77.59407 13.09533)\n", 219 | "8 NaN LINESTRING (77.59420 13.09518, 77.59427 13.09530)\n", 220 | "9 NaN LINESTRING (77.58396 12.99330, 77.58370 12.99343)\n", 221 | "10 Kumara Krupa Road LINESTRING (77.58396 12.99330, 77.58405 12.993...\n", 222 | "11 Kumara Krupa Road LINESTRING (77.58396 12.99330, 77.58384 12.993...\n", 223 | "12 NaN LINESTRING (77.57933 12.98596, 77.57841 12.98659)\n", 224 | "13 Kumara Krupa Road LINESTRING (77.57933 12.98596, 77.57951 12.98620)\n", 225 | "14 Kumara Krupa Road LINESTRING (77.57933 12.98596, 77.57919 12.98580)\n", 226 | "15 Bazaar st LINESTRING (77.62001 12.97471, 77.62009 12.97471)\n", 227 | "16 Bhaskaran Road LINESTRING (77.62001 12.97471, 77.62001 12.97475)\n", 228 | "17 Halasuru Road LINESTRING (77.62001 12.97471, 77.61913 12.974...\n", 229 | "18 Bhaskaran Road LINESTRING (77.62023 12.97568, 77.62025 12.97576)\n", 230 | "19 Gangadhar Chetty Road LINESTRING (77.62023 12.97568, 77.62010 12.97589)\u001b[0m\n", 231 | "\u001b[32;1m\u001b[1;3m\u001b[0m\n", 232 | "\n", 233 | "\u001b[1m> Finished chain.\u001b[0m\n" 234 | ] 235 | } 236 | ], 237 | "source": [ 238 | "r = agent(\"Find all bike roads in Bangalore, India\")" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 11, 244 | "id": "97b02be6-9bc6-4be3-aa76-313c5ad82f9a", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "r[\"output\"].to_file(\"roads.geojson\", driver=\"GeoJSON\")" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 13, 254 | "id": "fcf0c1d1-9005-4163-9169-4684a3f7fe27", 255 | "metadata": {}, 256 | "outputs": [ 257 | { 258 | "data": { 259 | "text/plain": [ 260 | "" 261 | ] 262 | }, 263 | "execution_count": 13, 264 | "metadata": {}, 265 | "output_type": "execute_result" 266 | }, 267 | { 268 | "data": { 269 | "image/png": "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", 270 | "text/plain": [ 271 | "
" 272 | ] 273 | }, 274 | "metadata": {}, 275 | "output_type": "display_data" 276 | } 277 | ], 278 | "source": [ 279 | "r[\"output\"].plot()" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "id": "94f4e4a8-99ff-49ee-add4-47b5f95c3842", 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [] 289 | } 290 | ], 291 | "metadata": { 292 | "kernelspec": { 293 | "display_name": "Python 3 (ipykernel)", 294 | "language": "python", 295 | "name": "python3" 296 | }, 297 | "language_info": { 298 | "codemirror_mode": { 299 | "name": "ipython", 300 | "version": 3 301 | }, 302 | "file_extension": ".py", 303 | "mimetype": "text/x-python", 304 | "name": "python", 305 | "nbconvert_exporter": "python", 306 | "pygments_lexer": "ipython3", 307 | "version": "3.11.3" 308 | } 309 | }, 310 | "nbformat": 4, 311 | "nbformat_minor": 5 312 | } 313 | -------------------------------------------------------------------------------- /nbs/23-06-28_stac-tool.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "44d97402-afb7-406f-8126-7e09fb8c488d", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import sys\n", 11 | "sys.path.append(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "86a490d4-0db2-490e-9907-f319614e8c1d", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import os\n", 22 | "from getpass import getpass\n", 23 | "\n", 24 | "import geopandas as gpd\n", 25 | "from langchain.chat_models import ChatOpenAI\n", 26 | "from langchain.agents import AgentType\n", 27 | "from tools.stac.search import STACSearchTool\n", 28 | "from tools.osmnx.geometry import OSMnxGeometryTool\n", 29 | "from agents.l4m_agent import base_agent" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "id": "af2b424e-c09b-484b-85e5-0ae8def2214a", 36 | "metadata": {}, 37 | "outputs": [ 38 | { 39 | "name": "stdout", 40 | "output_type": "stream", 41 | "text": [ 42 | " ········\n" 43 | ] 44 | } 45 | ], 46 | "source": [ 47 | "OPENAI_API_KEY = getpass()" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 4, 53 | "id": "836de1cb-71e3-46f9-95c8-042b8e36dda9", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 39, 63 | "id": "2f594e81-cd61-45cb-a184-eb6f8653b9ca", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "search_tool = STACSearchTool()\n", 68 | "geometry_tool = OSMnxGeometryTool()" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 40, 74 | "id": "5e8f5ea4-e4c0-4f60-b530-05ab70d5674d", 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "llm = llm = ChatOpenAI(\n", 79 | " temperature=0,\n", 80 | " openai_api_key=OPENAI_API_KEY,\n", 81 | " model_name=\"gpt-3.5-turbo\"\n", 82 | ")" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 41, 88 | "id": "28e0307e-4d62-49a9-a922-10091d3ddf10", 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "agent = base_agent(llm, \n", 93 | " tools=[search_tool, geometry_tool], \n", 94 | " agent_type=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 42, 100 | "id": "a96ebe7c-01d5-4829-bff6-6ee16f4376fa", 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "\n", 108 | "\n", 109 | "\u001b[1m> Entering new chain...\u001b[0m\n", 110 | "\u001b[32;1m\u001b[1;3mThought: To find all the hospitals in Bangalore, Karnataka, I can use the \"geometry\" tool with the relevant tags for hospitals in OpenStreetMap.\n", 111 | "\n", 112 | "Action:\n", 113 | "```\n", 114 | "{\n", 115 | " \"action\": \"geometry\",\n", 116 | " \"action_input\": {\n", 117 | " \"place\": \"Bangalore\",\n", 118 | " \"tags\": {\n", 119 | " \"amenity\": \"hospital\"\n", 120 | " }\n", 121 | " }\n", 122 | "}\n", 123 | "```\u001b[0m\n", 124 | "Observation: \u001b[33;1m\u001b[1;3m name \\\n", 125 | "0 R V Dental College & Hospital \n", 126 | "1 Chinmaya Mission Hospital \n", 127 | "2 KLE Dental College \n", 128 | "3 Health Centre \n", 129 | "4 K C General Hospital \n", 130 | ".. ... \n", 131 | "95 BBMP Maternity Hospital \n", 132 | "96 Ramakrishna Clinic \n", 133 | "97 ESI Hospital \n", 134 | "98 Apollo Hospital \n", 135 | "99 Panacea Hospitals Private Limited \n", 136 | "\n", 137 | " geometry \n", 138 | "0 POLYGON ((77.58435 12.91216, 77.58563 12.91212... \n", 139 | "1 POLYGON ((77.64580 12.97844, 77.64581 12.97838... \n", 140 | "2 POLYGON ((77.53492 13.03039, 77.53540 13.03118... \n", 141 | "3 POLYGON ((77.56370 13.01569, 77.56390 13.01569... \n", 142 | "4 POLYGON ((77.57087 12.99349, 77.56963 12.99342... \n", 143 | ".. ... \n", 144 | "95 POLYGON ((77.59690 12.94964, 77.59726 12.94932... \n", 145 | "96 POLYGON ((77.57441 12.97873, 77.57449 12.97874... \n", 146 | "97 POLYGON ((77.58934 12.92808, 77.58947 12.92808... \n", 147 | "98 POLYGON ((77.57240 12.98842, 77.57247 12.98841... \n", 148 | "99 POLYGON ((77.53860 12.99293, 77.53876 12.99287... \n", 149 | "\n", 150 | "[100 rows x 2 columns]\u001b[0m\n", 151 | "\u001b[32;1m\u001b[1;3m\u001b[0m\n", 152 | "\n", 153 | "\u001b[1m> Finished chain.\u001b[0m\n" 154 | ] 155 | } 156 | ], 157 | "source": [ 158 | "r = agent(\"find all the hospitals in Bangalore, Karnata\")" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 46, 164 | "id": "7a99af60-7c2d-4bc8-8506-c43d845d9d6d", 165 | "metadata": {}, 166 | "outputs": [ 167 | { 168 | "ename": "AttributeError", 169 | "evalue": "'dict' object has no attribute 'plot'", 170 | "output_type": "error", 171 | "traceback": [ 172 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 173 | "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", 174 | "Cell \u001b[0;32mIn[46], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplot\u001b[49m()\n", 175 | "\u001b[0;31mAttributeError\u001b[0m: 'dict' object has no attribute 'plot'" 176 | ] 177 | } 178 | ], 179 | "source": [ 180 | "r.plot()" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 36, 186 | "id": "f400544d-003a-4614-bb87-3738503d71bd", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "df = gpd.GeoDataFrame.from_features(r[\"output\"].to_dict(), crs=\"epsg:4326\")" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 37, 196 | "id": "eb9f2586-ae50-4a49-abf6-91777ad013d2", 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "data": { 201 | "text/plain": [ 202 | "" 203 | ] 204 | }, 205 | "execution_count": 37, 206 | "metadata": {}, 207 | "output_type": "execute_result" 208 | }, 209 | { 210 | "data": { 211 | "image/png": "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", 212 | "text/plain": [ 213 | "
" 214 | ] 215 | }, 216 | "metadata": {}, 217 | "output_type": "display_data" 218 | } 219 | ], 220 | "source": [ 221 | "df.plot(edgecolor=\"red\", facecolor=\"none\")" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "id": "c9c00270-fa00-4522-969e-1901b5150d2e", 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [] 231 | } 232 | ], 233 | "metadata": { 234 | "kernelspec": { 235 | "display_name": "Python 3 (ipykernel)", 236 | "language": "python", 237 | "name": "python3" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 3 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython3", 249 | "version": "3.11.4" 250 | } 251 | }, 252 | "nbformat": 4, 253 | "nbformat_minor": 5 254 | } 255 | -------------------------------------------------------------------------------- /tools/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/llllm/40d2d73d063a72c52babb56dfa5343b7d41dc8ac/tools/.gitkeep -------------------------------------------------------------------------------- /tools/geopy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/llllm/40d2d73d063a72c52babb56dfa5343b7d41dc8ac/tools/geopy/__init__.py -------------------------------------------------------------------------------- /tools/geopy/distance.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from geopy.distance import distance 4 | from pydantic import BaseModel, Field 5 | from langchain.tools import BaseTool 6 | 7 | 8 | class GeopyDistanceInput(BaseModel): 9 | """Input for GeopyDistanceTool.""" 10 | 11 | point_1: tuple[float, float] = Field(..., description="lat,lng of a place") 12 | point_2: tuple[float, float] = Field(..., description="lat,lng of a place") 13 | 14 | 15 | class GeopyDistanceTool(BaseTool): 16 | """Custom tool to calculate geodesic distance between two points.""" 17 | 18 | name: str = "distance" 19 | args_schema: Type[BaseModel] = GeopyDistanceInput 20 | description: str = "Use this tool to compute distance between two points available in lat,lng format." 21 | 22 | def _run(self, point_1: tuple[int, int], point_2: tuple[int, int]) -> float: 23 | return ("distance", distance(point_1, point_2).km) 24 | 25 | def _arun(self, place: str): 26 | raise NotImplementedError 27 | -------------------------------------------------------------------------------- /tools/geopy/geocode.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from geopy.geocoders import Nominatim 4 | from pydantic import BaseModel, Field 5 | from langchain.tools import BaseTool 6 | 7 | 8 | class GeopyGeocodeInput(BaseModel): 9 | """Input for GeopyGeocodeTool.""" 10 | 11 | place: str = Field(..., description="name of a place") 12 | 13 | 14 | class GeopyGeocodeTool(BaseTool): 15 | """Custom tool to perform geocoding.""" 16 | 17 | name: str = "geocode" 18 | args_schema: Type[BaseModel] = GeopyGeocodeInput 19 | description: str = "Use this tool for geocoding." 20 | 21 | def _run(self, place: str) -> tuple: 22 | locator = Nominatim(user_agent="geocode") 23 | location = locator.geocode(place) 24 | if location is None: 25 | return ("geocode", "Not a recognised address in Nomatim.") 26 | return ("geocode", (location.latitude, location.longitude)) 27 | 28 | def _arun(self, place: str): 29 | raise NotImplementedError 30 | -------------------------------------------------------------------------------- /tools/mercantile_tool.py: -------------------------------------------------------------------------------- 1 | import mercantile 2 | from langchain.tools import BaseTool 3 | 4 | 5 | class MercantileTool(BaseTool): 6 | """Tool to perform mercantile operations.""" 7 | 8 | name = "mercantile" 9 | description = "use this tool to get the xyz tiles for a place. \ 10 | To use this tool you need to provide lng,lat,zoom level of the place separated by comma." 11 | 12 | def _run(self, query): 13 | lng, lat, zoom = map(float, query.split(",")) 14 | return ("mercantile", mercantile.tile(lng, lat, zoom)) 15 | 16 | def _arun(self, query): 17 | raise NotImplementedError( 18 | "Mercantile tool doesn't have an async implementation." 19 | ) 20 | -------------------------------------------------------------------------------- /tools/osmnx/geometry.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Dict 2 | 3 | import osmnx as ox 4 | import geopandas as gpd 5 | from pydantic import BaseModel, Field 6 | from langchain.tools import BaseTool 7 | 8 | 9 | class PlaceWithTags(BaseModel): 10 | "Name of a place on the map and tags in OSM." 11 | 12 | place: str = Field(..., description="name of a place on the map.") 13 | tags: Dict[str, str] = Field(..., description="open street maps tags.") 14 | 15 | 16 | class OSMnxGeometryTool(BaseTool): 17 | """Tool to query geometries from Open Street Map (OSM).""" 18 | 19 | name: str = "geometry" 20 | args_schema: Type[BaseModel] = PlaceWithTags 21 | description: str = "Use this tool to get geometry of different features of the place like building footprints, parks, lakes, hospitals, schools etc. \ 22 | Pass the name of the place & tags of OSM as args." 23 | return_direct = True 24 | 25 | def _run(self, place: str, tags: Dict[str, str]) -> gpd.GeoDataFrame: 26 | gdf = ox.geometries_from_place(place, tags) 27 | gdf = gdf[gdf["geometry"].type.isin({"Polygon", "MultiPolygon"})] 28 | gdf = gdf[["name", "geometry"]].reset_index(drop=True) 29 | return ("geometry", gdf) 30 | 31 | def _arun(self, place: str): 32 | raise NotImplementedError 33 | -------------------------------------------------------------------------------- /tools/osmnx/network.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Dict 2 | 3 | import osmnx as ox 4 | from osmnx import utils_graph 5 | import geopandas as gpd 6 | from pydantic import BaseModel, Field 7 | from langchain.tools import BaseTool 8 | 9 | 10 | class PlaceWithNetworktype(BaseModel): 11 | "Name of a place on the map" 12 | place: str = Field(..., description="name of a place on the map") 13 | network_type: str = Field( 14 | ..., description="network type: one of walk, bike, drive or all" 15 | ) 16 | 17 | 18 | class OSMnxNetworkTool(BaseTool): 19 | """Custom tool to query road networks from OSM.""" 20 | 21 | name: str = "network" 22 | args_schema: Type[BaseModel] = PlaceWithNetworktype 23 | description: str = "Use this tool to get road network of a place. \ 24 | Pass the name of the place & type of road network i.e walk, bike, drive or all." 25 | return_direct = True 26 | 27 | def _run(self, place: str, network_type: str) -> gpd.GeoDataFrame: 28 | G = ox.graph_from_place(place, network_type=network_type, simplify=True) 29 | network = utils_graph.graph_to_gdfs(G, nodes=False) 30 | network = network[["name", "geometry"]].reset_index(drop=True) 31 | return ("network", network) 32 | 33 | def _arun(self, place: str): 34 | raise NotImplementedError 35 | -------------------------------------------------------------------------------- /tools/stac/search.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from pystac_client import Client 4 | import planetary_computer as pc 5 | from pydantic import BaseModel, Field 6 | from langchain.tools import BaseTool 7 | 8 | PC_STAC_API = "https://planetarycomputer.microsoft.com/api/stac/v1" 9 | 10 | 11 | class PlaceWithDatetimeAndBBox(BaseModel): 12 | "Name of a place and date." 13 | 14 | bbox: str = Field(..., description="bbox of the place") 15 | datetime: str = Field(..., description="datetime for the stac catalog search") 16 | 17 | 18 | class STACSearchTool(BaseTool): 19 | """Tool to search for STAC items in a catalog.""" 20 | 21 | name: str = "stac-search" 22 | args_schema: Type[BaseModel] = PlaceWithDatetimeAndBBox 23 | description: str = "Use this tool to search for STAC items in a catalog. \ 24 | Pass the bbox of the place & date as args." 25 | return_direct = True 26 | 27 | def _run(self, bbox: str, datetime: str): 28 | catalog = Client.open(PC_STAC_API, modifier=pc.sign_inplace) 29 | 30 | search = catalog.search( 31 | collections=["sentinel-2-l2a"], 32 | bbox=bbox, 33 | datetime=datetime, 34 | max_items=10, 35 | ) 36 | items = search.get_all_items() 37 | 38 | return ("stac-search", items) 39 | 40 | def _arun(self, bbox: str, datetime: str): 41 | raise NotImplementedError 42 | --------------------------------------------------------------------------------