Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
62b5a0d
Merge pull request #30 from PromptSail/dev
ksopyla May 14, 2024
3001180
Merge pull request #33 from PromptSail/dev
ksopyla May 18, 2024
52abe49
Merge pull request #34 from PromptSail/dev
ksopyla May 18, 2024
aa9e452
Merge pull request #35 from PromptSail/dev
ksopyla May 29, 2024
20f2c0e
Merge pull request #36 from PromptSail/dev
ksopyla Jun 16, 2024
88ea93e
Add info about try-promptsail demo page
ksopyla Jun 21, 2024
970004b
Merge pull request #38 from PromptSail/dev
ksopyla Jul 4, 2024
9cc683f
Merge pull request #39 from PromptSail/dev
WiolaGreen Jul 5, 2024
1c9eb79
bump lato version
ksopyla Oct 19, 2024
760aa85
add title and descritpion to swagger api docs
ksopyla Oct 19, 2024
d8be236
implicite MongoDatabase
ksopyla Oct 19, 2024
f15850a
bump libraries
ksopyla Oct 19, 2024
2b4ad2c
bump libraries
ksopyla Oct 19, 2024
e0c4128
update UI version fix warnings
ksopyla Oct 19, 2024
d49832d
fix headers
ksopyla Oct 19, 2024
c8fffc8
new updated UI docker image
ksopyla Oct 19, 2024
316845a
change the time periods
ksopyla Oct 19, 2024
0705985
Fix the docstring format for api endpoints to proper rendering in swa…
ksopyla Oct 27, 2024
f0d4790
set the openapi version
ksopyla Oct 27, 2024
97eccc7
Refactor, add test to create transations, move the price list to conf…
ksopyla Nov 1, 2024
605a5f2
Refactor the test count statistics. Move test_transaction files to fi…
ksopyla Nov 2, 2024
3bcb580
fix imports, move truncate_float to testt_utils
ksopyla Nov 2, 2024
61b9f07
Add refactored test_count_statistics_refactored.py
ksopyla Nov 2, 2024
0b4cba2
change to more meaningful names of request and response
ksopyla Nov 3, 2024
fe3247a
add date format to docs
ksopyla Nov 3, 2024
6f15c68
change to more meaningful names the request, response params in store…
ksopyla Nov 3, 2024
091656e
small refactor the read_transactions_from_csv
ksopyla Nov 3, 2024
3348d20
add docstrings and change the arrnage section to use read_transaction…
ksopyla Nov 3, 2024
003cad9
refactor names in token and cost statistics, create new Test Case for…
ksopyla Nov 3, 2024
ed8db1c
add test for transation cost calculation
ksopyla Nov 3, 2024
d1887c9
remove the key
ksopyla Nov 3, 2024
738244a
remove the test key
ksopyla Nov 3, 2024
bad5f81
add generation speed tests
ksopyla Nov 3, 2024
9f44020
Update Dockerfile
ksopyla Nov 3, 2024
ae5baaa
refactor test_api dostring and names
ksopyla Nov 9, 2024
44bce8e
refactor test file names, separate transaction tests from project, re…
ksopyla Nov 9, 2024
f7824f6
refactor transaction speed tests
ksopyla Nov 9, 2024
24bd0da
refactor the speed statistics, add missing test with error handling
ksopyla Nov 9, 2024
4c939a9
make dates for statistics speed requrired, remove the check_dates_for…
ksopyla Nov 9, 2024
ca0b897
Merge branch 'transaction-cost-refactor' into dev
ksopyla Nov 10, 2024
9cc1cb0
Add tests for different time granulairyt and periods checking the dat…
ksopyla Nov 10, 2024
f30c40b
wip: refactorization for speed statistics aggregation, refactore the …
ksopyla Nov 11, 2024
5007947
fix for storing transations via revers proxy
ksopyla Nov 16, 2024
d942aba
add exception showing in the logs
ksopyla Nov 16, 2024
74d1697
work on more tests, some refactoring add test for porfolio
ksopyla Nov 16, 2024
11330b8
Merge pull request #46 from PromptSail/dev
ksopyla Nov 16, 2024
a3c90d2
Create 2025-06-15-PromptSail-has-stoped.md
ksopyla Jun 15, 2025
0e864e9
Merge pull request #55 from PromptSail/docs
ksopyla Jun 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ To learn more about Prompt Sail’s features and capabilities, see
* [Langchain SDK -> Azure OpenAI](/examples/langchain_azure_openai.ipynb)
* [Langchain SDK -> Azure OpenAI Ada embeddings](/examples/langchain_azure_oai_embeddings.ipynb)

<!-- * [API Reference](https://promptsail.github.io/prompt_sail/api/). -->
* [API Reference](https://try-promptsail.azurewebsites.net/api/docs). -->



## Getting started 🚀

Checkout the documenttion [how to run PromptSail locally via docker.](https://promptsail.com/docs/quick-start-guide/)
The easiest way is to test our demo at **https://try-promptsail.azurewebsites.net/** (every new deployment erases the database)

Check out the documentation [how to run PromptSail locally via docker.](https://promptsail.com/docs/quick-start-guide/)


### Run Prompt Sail locally via Docker Compose 🐳
Expand Down
3 changes: 1 addition & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH

COPY src /src/
COPY static /static/
COPY provider_price_list.json test_transactions.csv ./
WORKDIR /src
EXPOSE 8000
# Run the application
CMD uvicorn app:app --proxy-headers --host 0.0.0.0 --port=${PORT:-8000}
#CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app:app"]
#CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app:app"]
8 changes: 4 additions & 4 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "PromptSail"
version = "0.1.0"
description = "Prompt Sail - prompt management and monitoring tool"
name = "PromptSail-backend"
version = "0.5.4"
description = "Prompt Sail - LLM governance, monitoring and analysis system"
authors = ["Przemysław Górecki <[email protected]>, Krzysztof Sopyła <[email protected]>"]

[tool.poetry.dependencies]
Expand All @@ -15,7 +15,7 @@ pydantic-settings = "^2.0.3"
gunicorn = "^22.0.0"
brotli = "^1.1.0"
pandas = "^2.2.1"
lato = "^0.8.0"
lato = "^0.11.0"
python-slugify = "^8.0.4"
fastapi = "^0.110.1"
pyzmq = "~25"
Expand Down
9 changes: 7 additions & 2 deletions backend/src/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async def fastapi_lifespan(app: FastAPI):
ctx.call(add_project, data2)

if settings_repository.count() == 0:
organization_name = os.getenv("ORGANIZATION_NAME", None)
organization_name = os.getenv("ORGANIZATION_NAME", "PromptSail")

if organization_name is not None:
data = OrganizationSettings(
Expand All @@ -80,5 +80,10 @@ async def fastapi_lifespan(app: FastAPI):
...


app = FastAPI(lifespan=fastapi_lifespan)
app = FastAPI(lifespan=fastapi_lifespan,
title="PromptSail API",
description="API for PromptSail - prompt management and monitoring tool",
version="0.5.4",
openapi_version="3.1.0",
)
app.container = container
130 changes: 66 additions & 64 deletions backend/src/app/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,39 @@
# return response


if config.DEBUG:

@app.middleware("exception_handler")
async def __call__(request: Request, call_next):
"""
Middleware for managing exception handling.

:param request: The incoming request.
:param call_next: The callable representing the next middleware or endpoint in the chain.
:return: The response from the middleware or endpoint.
"""
logger = get_logger(request)
try:
return await call_next(request)
except HTTPException as http_exception:
return JSONResponse(
status_code=http_exception.status_code,
content={
"error": "Client Error",
"messages": str(http_exception.detail),
},
)
except Exception as e:
logger.exception(f"Error message: {e.__class__.__name__}. Args: {e.args}")
return JSONResponse(
status_code=500,
content={
"error": "Internal Server Error",
"message": "An unexpected error occurred.",
},
)
# if config.DEBUG:

@app.middleware("exception_handler")
async def __call__(request: Request, call_next):
"""
Middleware for managing exception handling.

:param request: The incoming request.
:param call_next: The callable representing the next middleware or endpoint in the chain.
:return: The response from the middleware or endpoint.
"""
logger = get_logger(request)
try:
return await call_next(request)
except HTTPException as http_exception:
logger.exception(f"HttpExcepion occures {http_exception.status_code} - {http_exception.detail}")
raise http_exception
# return JSONResponse(
# status_code=http_exception.status_code,
# content={
# "error": "Client Error",
# "messages": str(http_exception.detail),
# },
# )
except Exception as e:
logger.exception(f"Error message: {e.__class__.__name__}. Args: {e.args}")
return JSONResponse(
status_code=500,
content={
"error": "Internal Server Error",
"message": "An unexpected error occurred.",
},
)


@app.middleware("transaction_context")
Expand All @@ -78,38 +80,38 @@ async def __call__(request: Request, call_next):
return response


@app.middleware("proxy_tunnel")
async def __call__(request: Request, call_next):
"""
Middleware for handling proxy tunnel requests.

:param request: The incoming request.
:param call_next: The callable representing the next middleware or endpoint in the chain.
:return: The response from the middleware or endpoint.
"""
if request.method == "CONNECT":
# Parse the host and port from the request's path
host, port = request.scope.get("path").split(":")
port = int(port)

print("proxy_tunnel", host, port)

raise NotImplementedError(
"Using PromptSail as a true proxy is not supported yet"
)

# Create a socket connection to the target server
# client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# await request.send({"type": "http.response.start", "status": 200})
# @app.middleware("proxy_tunnel")
# async def __call__(request: Request, call_next):
# """
# Middleware for handling proxy tunnel requests.

# try:
# await request.app.proxy_tunnel(client_socket, host, port)
# except Exception as e:
# print(f"Error during proxy tunnel: {e}")
# finally:
# client_socket.close()
#
# return Response(content=b"", status_code=200)
# :param request: The incoming request.
# :param call_next: The callable representing the next middleware or endpoint in the chain.
# :return: The response from the middleware or endpoint.
# """
# if request.method == "CONNECT":
# # Parse the host and port from the request's path
# host, port = request.scope.get("path").split(":")
# port = int(port)

# print("proxy_tunnel", host, port)

# raise NotImplementedError(
# "Using PromptSail as a true proxy is not supported yet"
# )

# # Create a socket connection to the target server
# # client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # await request.send({"type": "http.response.start", "status": 200})

# # try:
# # await request.app.proxy_tunnel(client_socket, host, port)
# # except Exception as e:
# # print(f"Error during proxy tunnel: {e}")
# # finally:
# # client_socket.close()
# #
# # return Response(content=b"", status_code=200)

response = await call_next(request)
return response
# response = await call_next(request)
# return response
102 changes: 63 additions & 39 deletions backend/src/app/reverse_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ async def iterate_stream(response, buffer):
"""
Asynchronously iterate over the raw stream of a response and accumulate chunks in a buffer.

:param response: The response object.
:param buffer: The buffer to accumulate chunks.
:return: An asynchronous generator yielding chunks from the response stream.
This function asynchronously iterate over the raw stream of a response and accumulate chunks in a buffer
for later processing.

Parameters:
- **response**: The response object containing the stream
- **buffer**: List to store the accumulated response chunks

Yields:
- Chunks of the response data as they are received
"""
async for chunk in response.aiter_raw():
buffer.append(chunk)
Expand All @@ -31,34 +37,38 @@ async def iterate_stream(response, buffer):
async def close_stream(
app: Application,
project_id,
request,
response,
ai_provider_request,
ai_provider_response,
buffer,
tags,
ai_model_version,
pricelist,
request_time,
):
"""
Asynchronously close the response stream and store the transaction in the database.

:param app: The Application instance.
:param project_id: The Project ID.
:param request: The incoming request.
:param response: The response object.
:param buffer: The buffer containing accumulated chunks.
:param tags: The tags associated with the transaction.
:param ai_model_version: Specific tag for AI model. Helps with cost count.
:param pricelist: The pricelist for the models.
:param request_time: The request time.
Process and store transaction data after stream completion.

This function handles the post-streaming tasks, including storing transaction details
and raw request/response data in the database.

Parameters:
- **app**: The Application instance
- **project_id**: The unique identifier of the project
- **ai_provider_request**: The original request object
- **ai_provider_response**: The response object from the AI provider
- **buffer**: Buffer containing the accumulated response data
- **tags**: List of tags associated with the transaction
- **ai_model_version**: Specific model version tag for cost calculation
- **pricelist**: List of provider prices for cost calculation
- **request_time**: Timestamp when the request was initiated
"""
await response.aclose()
await ai_provider_response.aclose()
with app.transaction_context() as ctx:
data = ctx.call(
store_transaction,
project_id=project_id,
request=request,
response=response,
ai_provider_request=ai_provider_request,
ai_provider_response=ai_provider_response,
buffer=buffer,
tags=tags,
ai_model_version=ai_model_version,
Expand All @@ -67,9 +77,9 @@ async def close_stream(
)
ctx.call(
store_raw_transactions,
request=request,
request=ai_provider_request,
request_content=data["request_content"],
response=response,
response=ai_provider_response,
response_content=data["response_content"],
transaction_id=data["transaction_id"],
)
Expand All @@ -90,17 +100,30 @@ async def reverse_proxy(
target_path: str | None = None,
):
"""
API route for reverse proxying requests to the upstream server.

:param project_slug: The slug of the project.
:param provider_slug: The slug of the AI provider.
:param path: The path for the reverse proxy.
:param request: The incoming request.
:param ctx: The transaction context dependency.
:param tags: Optional. Tags associated with the transaction.
:param ai_model_version: Optional. Specific tag for AI model. Helps with cost count.
:param target_path: Optional. Target path for the reverse proxy.
:return: A StreamingResponse object.
Forward requests to AI providers and handle responses.

This endpoint acts as a reverse proxy, forwarding requests to various AI providers
while monitoring and storing transaction details. It handles streaming responses,
calculates costs, and maintains transaction history.

Parameters:
- **project_slug**: The unique slug identifier of the project
- **provider_slug**: The slug identifier of the AI provider
- **path**: The API endpoint path to forward to
- **request**: The incoming request object
- **ctx**: The transaction context dependency
- **tags**: Optional comma-separated list of tags for the transaction
- **ai_model_version**: Optional specific model version for accurate cost calculation
- **target_path**: Optional override for the target API path

Returns:
- A StreamingResponse object containing the provider's response

Notes:
- Automatically handles request/response streaming
- Stores transaction details and raw data in the background
- Calculates costs based on the provider's pricing
- Supports various HTTP methods (GET, POST, PUT, PATCH, DELETE)
"""
logger = get_logger(request)

Expand All @@ -112,6 +135,7 @@ async def reverse_proxy(
project = ctx.call(get_project_by_slug, slug=project_slug)
url = ApiURLBuilder.build(project, provider_slug, path, target_path)

# todo: remove this, this logic should be in the use case
pricelist = get_provider_pricelist(request)

logger.debug(f"got projects for {project}")
Expand All @@ -125,7 +149,7 @@ async def reverse_proxy(
timeout = httpx.Timeout(100.0, connect=50.0)

request_time = datetime.now(tz=timezone.utc)
rp_req = client.build_request(
ai_provider_request = client.build_request(
method=request.method,
url=url,
headers={
Expand All @@ -136,19 +160,19 @@ async def reverse_proxy(
timeout=timeout,
)
logger.debug(f"Requesting on: {url}")
rp_resp = await client.send(rp_req, stream=True, follow_redirects=True)
ai_provider_response = await client.send(ai_provider_request, stream=True, follow_redirects=True)

buffer = []
return StreamingResponse(
iterate_stream(rp_resp, buffer),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
iterate_stream(ai_provider_response, buffer),
status_code=ai_provider_response.status_code,
headers=ai_provider_response.headers,
background=BackgroundTask(
close_stream,
ctx["app"],
project.id,
rp_req,
rp_resp,
ai_provider_request,
ai_provider_response,
buffer,
tags,
ai_model_version,
Expand Down
Loading
Loading