Skip to content
This repository was archived by the owner on Nov 13, 2024. It is now read-only.

Commit bf05ba3

Browse files
authored
add /api and version to app.py (#169)
* add /api and version to app.py * restructre api models to versionized directories * split to routers and set up app init funciton * fixed docs/schema * add version to /health * edit base url for tests to API_VESION * merge main * chage api_base on cli chat * edit comment * set default for default url path * revert to 0.0.0.0 on uvicorn * fix import in text
1 parent 020a3de commit bf05ba3

File tree

5 files changed

+83
-38
lines changed

5 files changed

+83
-38
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,12 @@ This will open a similar chat interface window, but will show both the RAG and n
197197

198198
### Migrating an existing OpenAI application to **Canopy**
199199

200-
If you already have an application that uses the OpenAI API, you can migrate it to **Canopy** by simply changing the API endpoint to `http://host:port/context` as follows:
200+
If you already have an application that uses the OpenAI API, you can migrate it to **Canopy** by simply changing the API endpoint to `http://host:port/v1`, for example with the default configuration:
201201

202202
```python
203203
import openai
204204

205-
openai.api_base = "http://host:port/"
205+
openai.api_base = "http://localhost:8000/v1"
206206

207207
# now you can use the OpenAI API as usual
208208
```
@@ -212,7 +212,7 @@ or without global state change:
212212
```python
213213
import openai
214214

215-
openai_response = openai.Completion.create(..., api_base="http://host:port/")
215+
openai_response = openai.Completion.create(..., api_base="http://localhost:8000/v1")
216216
```
217217

218218
### Running Canopy server in production

src/canopy_cli/cli.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@
3131

3232
from canopy import __version__
3333

34-
from canopy_server.app import start as start_server
34+
from canopy_server.app import start as start_server, API_VERSION
3535
from .cli_spinner import Spinner
36-
from canopy_server.api_models import ChatDebugInfo
36+
from canopy_server.models.v1.api_models import ChatDebugInfo
3737

3838

3939
load_dotenv()
4040
if os.getenv("OPENAI_API_KEY"):
4141
openai.api_key = os.getenv("OPENAI_API_KEY")
4242

43-
spinner = Spinner()
4443
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
44+
DEFAULT_SERVER_URL = f"http://localhost:8000/{API_VERSION}"
45+
spinner = Spinner()
4546

4647

4748
def check_server_health(url: str):
@@ -171,8 +172,9 @@ def cli(ctx):
171172

172173

173174
@cli.command(help="Check if canopy server is running and healthy.")
174-
@click.option("--url", default="http://localhost:8000",
175-
help="Canopy's server url. Defaults to http://localhost:8000")
175+
@click.option("--url", default=DEFAULT_SERVER_URL,
176+
help=("Canopy's server url. "
177+
f"Defaults to {DEFAULT_SERVER_URL}"))
176178
def health(url):
177179
check_server_health(url)
178180
click.echo(click.style("Canopy server is healthy!", fg="green"))
@@ -432,8 +434,9 @@ def _chat(
432434
help="Print additional debugging information")
433435
@click.option("--rag/--no-rag", default=True,
434436
help="Compare RAG-infused Chatbot with vanilla LLM",)
435-
@click.option("--chat-server-url", default="http://localhost:8000",
436-
help="URL of the Canopy server to use. Defaults to http://localhost:8000")
437+
@click.option("--chat-server-url", default=DEFAULT_SERVER_URL,
438+
help=("URL of the Canopy server to use."
439+
f" Defaults to {DEFAULT_SERVER_URL}"))
437440
def chat(chat_server_url, rag, debug, stream):
438441
check_server_health(chat_server_url)
439442
note_msg = (
@@ -488,7 +491,7 @@ def chat(chat_server_url, rag, debug, stream):
488491
history=history_with_pinecone,
489492
message=message,
490493
stream=stream,
491-
api_base=urljoin(chat_server_url, "/context"),
494+
api_base=chat_server_url,
492495
print_debug_info=debug,
493496
)
494497

@@ -527,7 +530,7 @@ def chat(chat_server_url, rag, debug, stream):
527530
)
528531
)
529532
@click.option("--host", default="0.0.0.0",
530-
help="Hostname or ip address to bind the server to. Defaults to 0.0.0.0")
533+
help="Hostname or address to bind the server to. Defaults to 0.0.0.0")
531534
@click.option("--port", default=8000,
532535
help="TCP port to bind the server to. Defaults to 8000")
533536
@click.option("--reload/--no-reload", default=False,
@@ -580,8 +583,9 @@ def start(host: str, port: str, reload: bool,
580583
"""
581584
)
582585
)
583-
@click.option("url", "--url", default="http://localhost:8000",
584-
help="URL of the Canopy server to use. Defaults to http://localhost:8000")
586+
@click.option("url", "--url", default=DEFAULT_SERVER_URL,
587+
help=("URL of the Canopy server to use. "
588+
f"Defaults to {DEFAULT_SERVER_URL}"))
585589
def stop(url):
586590
if os.name != "nt":
587591
# Check if the server was started using Gunicorn
@@ -643,17 +647,17 @@ def api_docs(url):
643647
if generated_docs:
644648
import json
645649
from canopy_server._redocs_template import HTML_TEMPLATE
646-
from canopy_server.app import app
650+
from canopy_server.app import app, _init_routes
647651
# generate docs
648-
652+
_init_routes(app)
649653
filename = "canopy-api-docs.html"
650654
msg = f"Generating docs to {filename}"
651655
click.echo(click.style(msg, fg="green"))
652656
with open(filename, "w") as fd:
653657
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
654658
webbrowser.open('file://' + os.path.realpath(filename))
655659
else:
656-
webbrowser.open('http://localhost:8000/redoc')
660+
webbrowser.open(urljoin(url, "redoc"))
657661

658662

659663
if __name__ == "__main__":

src/canopy_server/app.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
from starlette.concurrency import run_in_threadpool
1919
from sse_starlette.sse import EventSourceResponse
2020

21-
from fastapi import FastAPI, HTTPException, Body
21+
from fastapi import (
22+
FastAPI,
23+
HTTPException,
24+
Body,
25+
APIRouter
26+
)
2227
import uvicorn
2328
from typing import cast, Union
2429

@@ -27,7 +32,7 @@
2732
ChatResponse,
2833
)
2934
from canopy.models.data_models import Context, UserMessage
30-
from .api_models import (
35+
from .models.v1.api_models import (
3136
ChatRequest,
3237
ContextQueryRequest,
3338
ContextUpsertRequest,
@@ -64,8 +69,10 @@
6469
You can find your free trial OpenAI API key https://platform.openai.com/account/api-keys. You might need to log in or register for OpenAI services.
6570
""" # noqa: E501
6671

72+
API_VERSION = "v1"
6773

68-
app = FastAPI(
74+
# Global variables - Application
75+
app: FastAPI = FastAPI(
6976
title="Canopy API",
7077
description=APP_DESCRIPTION,
7178
version=__version__,
@@ -74,16 +81,22 @@
7481
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
7582
},
7683
)
84+
openai_api_router = APIRouter()
85+
context_api_router = APIRouter(prefix="/context")
86+
application_router = APIRouter(tags=["Application"])
7787

88+
# Global variables - Engines
7889
context_engine: ContextEngine
7990
chat_engine: ChatEngine
8091
kb: KnowledgeBase
8192
llm: BaseLLM
93+
94+
# Global variables - Logging
8295
logger: logging.Logger
8396

8497

85-
@app.post(
86-
"/context/chat/completions",
98+
@openai_api_router.post(
99+
"/chat/completions",
87100
response_model=None,
88101
responses={500: {"description": "Failed to chat with Canopy"}}, # noqa: E501
89102
)
@@ -126,8 +139,8 @@ def stringify_content(response: StreamingChatResponse):
126139
raise HTTPException(status_code=500, detail=f"Internal Service Error: {str(e)}")
127140

128141

129-
@app.post(
130-
"/context/query",
142+
@context_api_router.post(
143+
"/query",
131144
response_model=ContextResponse,
132145
responses={
133146
500: {"description": "Failed to query the knowledge base or build the context"}
@@ -156,8 +169,8 @@ async def query(
156169
raise HTTPException(status_code=500, detail=f"Internal Service Error: {str(e)}")
157170

158171

159-
@app.post(
160-
"/context/upsert",
172+
@context_api_router.post(
173+
"/upsert",
161174
response_model=SuccessUpsertResponse,
162175
responses={500: {"description": "Failed to upsert documents"}},
163176
)
@@ -183,8 +196,8 @@ async def upsert(
183196
raise HTTPException(status_code=500, detail=f"Internal Service Error: {str(e)}")
184197

185198

186-
@app.post(
187-
"/context/delete",
199+
@context_api_router.post(
200+
"/delete",
188201
response_model=SuccessDeleteResponse,
189202
responses={500: {"description": "Failed to delete documents"}},
190203
)
@@ -204,7 +217,7 @@ async def delete(
204217
raise HTTPException(status_code=500, detail=f"Internal Service Error: {str(e)}")
205218

206219

207-
@app.get(
220+
@application_router.get(
208221
"/health",
209222
response_model=HealthStatus,
210223
responses={500: {"description": "Failed to connect to Pinecone or LLM"}},
@@ -236,7 +249,7 @@ async def health_check() -> HealthStatus:
236249
return HealthStatus(pinecone_status="OK", llm_status="OK")
237250

238251

239-
@app.get("/shutdown")
252+
@application_router.get("/shutdown")
240253
async def shutdown() -> ShutdownResponse:
241254
"""
242255
__WARNING__: Experimental method.
@@ -267,6 +280,19 @@ async def shutdown() -> ShutdownResponse:
267280
async def startup():
268281
_init_logging()
269282
_init_engines()
283+
_init_routes(app)
284+
285+
286+
def _init_routes(app):
287+
# Include the application level router (health, shutdown, ...)
288+
app.include_router(application_router, include_in_schema=False)
289+
app.include_router(application_router, prefix=f"/{API_VERSION}")
290+
# Include the API without version == latest
291+
app.include_router(context_api_router, include_in_schema=False)
292+
app.include_router(openai_api_router, include_in_schema=False)
293+
# Include the API version in the path, API_VERSION should be the latest version.
294+
app.include_router(context_api_router, prefix=f"/{API_VERSION}", tags=["Context"])
295+
app.include_router(openai_api_router, prefix=f"/{API_VERSION}", tags=["LLM"])
270296

271297

272298
def _init_logging():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from canopy.models.data_models import Messages, Query, Document
66

7+
# TODO: consider separating these into modules: Chat, Context, Application, etc.
8+
79

810
class ChatRequest(BaseModel):
911
model: str = Field(

tests/e2e/test_app.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
from canopy.knowledge_base import KnowledgeBase
1313

14-
from canopy_server.app import app
15-
from canopy_server.api_models import (HealthStatus, ContextUpsertRequest,
16-
ContextQueryRequest, )
14+
from canopy_server.app import app, API_VERSION
15+
from canopy_server.models.v1.api_models import (
16+
HealthStatus,
17+
ContextUpsertRequest,
18+
ContextQueryRequest)
1719
from .. import Tokenizer
1820

1921
upsert_payload = ContextUpsertRequest(
@@ -63,6 +65,7 @@ def client(knowledge_base, index_name):
6365
os.environ["INDEX_NAME"] = index_name
6466
Tokenizer.clear()
6567
with TestClient(app) as client:
68+
client.base_url = f"{client.base_url}/{API_VERSION}"
6669
yield client
6770
if index_name_before:
6871
os.environ["INDEX_NAME"] = index_name_before
@@ -95,7 +98,9 @@ def test_health(client):
9598

9699
def test_upsert(client):
97100
# Upsert a document to the index
98-
upsert_response = client.post("/context/upsert", json=upsert_payload.dict())
101+
upsert_response = client.post(
102+
"/context/upsert",
103+
json=upsert_payload.dict())
99104
assert upsert_response.is_success
100105

101106

@@ -114,7 +119,9 @@ def test_query(client):
114119
max_tokens=100,
115120
)
116121

117-
query_response = client.post("/context/query", json=query_payload.dict())
122+
query_response = client.post(
123+
"/context/query",
124+
json=query_payload.dict())
118125
assert query_response.is_success
119126

120127
query_response = query_response.json()
@@ -142,7 +149,9 @@ def test_chat_required_params(client):
142149
}
143150
]
144151
}
145-
chat_response = client.post("/context/chat/completions", json=chat_payload)
152+
chat_response = client.post(
153+
"/chat/completions",
154+
json=chat_payload)
146155
assert chat_response.is_success
147156
chat_response_as_json = chat_response.json()
148157
assert chat_response_as_json["choices"][0]["message"]["role"] == "assistant"
@@ -170,7 +179,9 @@ def test_chat_openai_additional_params(client):
170179
"stop": "stop string",
171180
"top_p": 0.5,
172181
}
173-
chat_response = client.post("/context/chat/completions", json=chat_payload)
182+
chat_response = client.post(
183+
"/chat/completions",
184+
json=chat_payload)
174185
assert chat_response.is_success
175186
chat_response_as_json = chat_response.json()
176187
assert chat_response_as_json["choices"][0]["message"]["role"] == "assistant"
@@ -189,7 +200,9 @@ def test_delete(client, knowledge_base):
189200
delete_payload = {
190201
"document_ids": doc_ids
191202
}
192-
delete_response = client.post("/context/delete", json=delete_payload)
203+
delete_response = client.post(
204+
"/context/delete",
205+
json=delete_payload)
193206
assert delete_response.is_success
194207

195208
assert_vector_ids_not_exist(vector_ids, knowledge_base)

0 commit comments

Comments
 (0)