adding HTML components for HTMX integration

This commit is contained in:
Francis Secada 2025-01-17 17:10:15 -05:00
parent 012ef4cc04
commit 0e65c811a1
32 changed files with 32457 additions and 53 deletions

2
.gitignore vendored
View File

@ -160,4 +160,4 @@ celerybeat-schedule*
.ruff_cache
# JS dependencies
node_modules
node_modules

View File

@ -2,33 +2,24 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-ast
- id: check-builtin-literals
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: check-toml
- id: check-vcs-permalinks
- id: check-xml
- id: check-yaml
- id: check-builtin-literals
- id: debug-statements
exclude: tests/
- id: destroyed-symlinks
# - id: detect-aws-credentials
- id: detect-private-key
- id: end-of-file-fixer
exclude: tests/test_changes/
files: \.(py|sh|rst|yml|yaml)$
- id: fix-byte-order-marker
- id: pretty-format-json
args: [--autofix]
- id: sort-simple-yaml
- id: mixed-line-ending
- id: trailing-whitespace
- hooks:
- id: add-trailing-comma
repo: 'https://github.com/asottile/add-trailing-comma'
rev: v3.1.0
- hooks:
- id: pyupgrade
repo: 'https://github.com/asottile/pyupgrade'
rev: v3.19.1
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
@ -52,4 +43,4 @@ repos:
rev: v3.19.1
hooks:
- id: pyupgrade
args: [--py311-plus]
args: [--py313-plus]

View File

@ -34,4 +34,4 @@ RUN mkdir -p /tmp/log/celery && \
RUN find . -name "*.sh" -exec chmod +x {} \;
RUN echo $WORKDIR
RUN /bin/bash -c 'source $WORKDIR/docker/pygentic_ai/python_build.sh'
CMD /bin/bash -c 'source $WORKDIR/docker/pygentic_ai/python_start.sh'
CMD /bin/bash -c 'source $WORKDIR/docker/pygentic_ai/python_start.sh'

View File

@ -7,4 +7,4 @@ isort
jupyterlab
jupyterlab-code-formatter
pre-commit
ruff
ruff

View File

@ -20,7 +20,8 @@ app = create_app(debug=debug_arg, settings_obj=app_settings)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
request: Request,
exc: RequestValidationError,
):
"""
Custom validation error messaging for end-users. This reduces ambiguity
@ -35,7 +36,8 @@ async def validation_exception_handler(
content = {"status_code": 10422, "message": exc_str, "data": None}
return JSONResponse(
content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
content=content,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@ -63,7 +65,7 @@ async def unicorn_exception_handler(request: Request, exc: UnicornException):
status_code=418,
content={
"message": f"Oops! {exc.name} did something. "
"There goes a rainbow..."
"There goes a rainbow...",
},
)

View File

@ -10,7 +10,8 @@ from backend.logger import logger
@swot_agent.tool(prepare=report_tool_usage)
async def fetch_website_content(
_ctx: RunContext[SwotAgentDeps], url: str
_ctx: RunContext[SwotAgentDeps],
url: str,
) -> str:
"""
Fetches the HTML content of the given URL via httpx and beautifulsoup
@ -70,7 +71,9 @@ async def analyze_competition(
@swot_agent.tool(prepare=report_tool_usage)
async def get_reddit_insights(
ctx: RunContext[SwotAgentDeps], query: str, subreddit_name: str = "python"
ctx: RunContext[SwotAgentDeps],
query: str,
subreddit_name: str = "python",
):
"""
A tool to gain insights from a subreddit. Data is returned as string
@ -89,14 +92,15 @@ async def get_reddit_insights(
insights.append(
f"Title: {post.title}\n"
f"URL: {post.url}\n"
f"Content: {post.selftext}\n"
f"Content: {post.selftext}\n",
)
return "\n".join(insights)
async def run_agent(
url: str, deps: SwotAgentDeps = SwotAgentDeps()
url: str,
deps: SwotAgentDeps = SwotAgentDeps(),
) -> SwotAnalysis | Exception:
"""
Runs the SWOT Analysis Agent

View File

@ -5,7 +5,8 @@ from backend.core.core import SwotAgentDeps
async def report_tool_usage(
ctx: RunContext[SwotAgentDeps], tool_def: ToolDefinition
ctx: RunContext[SwotAgentDeps],
tool_def: ToolDefinition,
) -> ToolDefinition:
"""
Reports tool usage + results to an update function
@ -18,7 +19,8 @@ async def report_tool_usage(
if ctx.deps.update_status_func:
await ctx.deps.update_status_func(
ctx.deps.request, f"Using tool: {tool_def.name}..."
ctx.deps.request,
f"Using tool: {tool_def.name}...",
)
ctx.deps.tool_history.append(tool_def.name)

View File

@ -38,7 +38,9 @@ def as_form(cls: type[BaseModel]):
inspect.Parameter.POSITIONAL_ONLY,
default=model_field.default,
annotation=Annotated[
model_field.annotation, *model_field.metadata, Form()
model_field.annotation,
*model_field.metadata,
Form(),
],
)
for field_name, model_field in cls.model_fields.items()
@ -56,7 +58,7 @@ def as_form(cls: type[BaseModel]):
return cls
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def get_async_session() -> AsyncGenerator[AsyncSession]:
"""
Get async SQLA session via generator function
"""
@ -88,7 +90,7 @@ async def run_out_of_band(
"""
async with async_sessionaker() as oob_session:
await oob_session.connection(
execution_options={"isolation_level": "AUTOCOMMIT"}
execution_options={"isolation_level": "AUTOCOMMIT"},
)
result = await oob_session.execute(statement)
@ -96,7 +98,10 @@ async def run_out_of_band(
if merge_results:
return (
await session_inst.run_sync(
merge_frozen_result, statement, result.freeze(), load=False
merge_frozen_result,
statement,
result.freeze(),
load=False,
)
)()
else:
@ -114,7 +119,7 @@ async def check_db_exists(engine_inst):
from sqlalchemy import inspect # noqa
tables = await conn.run_sync(
lambda sync_conn: inspect(sync_conn).get_table_names()
lambda sync_conn: inspect(sync_conn).get_table_names(),
)
logger.info(tables)
if not tables:
@ -146,7 +151,9 @@ async def create_db(
def create_db_engine(
db_url: str, async_bool: bool = False, echo_bool: bool = False
db_url: str,
async_bool: bool = False,
echo_bool: bool = False,
):
"""
@ -194,11 +201,15 @@ sync_engine = create_db_engine(db_url, echo_bool=echo)
session_inst = Session(sync_engine, expire_on_commit=False)
async_session_maker = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
session_maker = sessionmaker(
bind=engine, class_=Session, expire_on_commit=False
bind=engine,
class_=Session,
expire_on_commit=False,
)
if __name__ == "__main__":

View File

@ -26,7 +26,7 @@ list(
enqueue=True,
),
["info", "debug", "error", "success", "warning", "critical"],
)
),
)
@ -53,7 +53,8 @@ class InterceptHandler(logging.Handler):
log = logger.bind(request_id="app")
log.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
level,
record.getMessage(),
)

View File

@ -7,7 +7,8 @@ from backend.settings.prod import Settings as ProdSettings
from backend.utils import get_val
server_types = enum.StrEnum(
"ServerTypes", {x.upper(): x for x in ("dev", "uat", "staging", "prod")}
"ServerTypes",
{x.upper(): x for x in ("dev", "uat", "staging", "prod")},
)

View File

@ -56,7 +56,7 @@ def get_db_val(
[
hasattr(db_configs, x)
for x in ("name", "user", "password", "host", "port", "kwargs")
]
],
):
url = f"{dialect}:{schema_sep}{db_configs.host}"
else:

View File

@ -47,7 +47,7 @@ all_dialects = enum.Enum(
y
for x in (pg_dialects, mysql_dialects, sqlite_dialects)
for y in x
]
],
]
},
)

View File

@ -26,10 +26,10 @@ catalog = Catalog(jinja_env=templates.env)
list(
map(
lambda folder: catalog.add_folder(
os.path.join(frontend, "components", folder) # noqa
os.path.join(frontend, "components", folder), # noqa
),
("main", "forms", "snippets"),
)
),
)
user_frontend.mount(
@ -87,7 +87,7 @@ async def get_status(request: Request):
result = ANALYSIS_COMPLETE_MESSAGE in messages
logger.info(
f"Status check - Session ID: {'session_id'}, Messages: "
f"{messages}"
f"{messages}",
)
context.update({"messages": messages, "result": result})

View File

@ -46,11 +46,14 @@ async def update_status(session_id: str, message: Any) -> None:
else:
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None, emulate_tool_completion, session_id, message
None,
emulate_tool_completion,
session_id,
message,
)
logger.info(
f"Status messages for session {session_id}: {status_store[session_id]}"
f"Status messages for session {session_id}: {status_store[session_id]}",
)
@ -67,7 +70,8 @@ async def run_agent_with_progress(session_id, url):
deps = SwotAgentDeps(
request=None,
update_status_func=lambda request, msg: update_status(
session_id, msg
session_id,
msg,
),
)
@ -78,7 +82,7 @@ async def run_agent_with_progress(session_id, url):
result_store[session_id] = result
except Exception as e:
logger.error(
f"An unexpected error occurred. See here: " f"{type(e), e, e.args}"
f"An unexpected error occurred. See here: " f"{type(e), e, e.args}",
)
await update_status(session_id, f"Unexpected error: {e}")
raise

View File

@ -62,7 +62,7 @@ def get_val(val: str, default: str | int | bool | None = None, **kwargs):
else:
raise ValueError(
f"Env Var {val} is not populated in the environment "
f"or within the configuration files"
f"or within the configuration files",
)
return val

View File

@ -0,0 +1 @@
@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.is-collapsible{overflow-y:hidden;transition:height .2s ease}.is-collapsible.is-active{transition:height .2s ease}.is-collapsible.message-body{padding:0!important}.is-collapsible.message-body .message-body-content{padding:1.25em 1.5em}

21559
src/frontend/static/css/bulma.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

3
src/frontend/static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
src/frontend/static/css/bulma.scss vendored Normal file
View File

@ -0,0 +1,4 @@
@charset "utf-8";
/*! bulma.io v1.0.3 | MIT License | github.com/jgthms/bulma */
@use "sass";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,56 @@
#!/usr/bin/env node
// build a --template, or a whole project --root directory
// default context filename is `index.js` for dirs, unless --context
// specify a --build directory to output there, instead of STDOUT
var
preprocess= require('./preprocess'),
// here you can pre-process the response before final output
DELIM= ["::", "::"], // or use ["<\\?js", "\\?>"] to get `1<?js2?>3` → `123`
// describe your document context in JSON, then use <?js ?> for the template
// i.e. root/index.json {a:1, b:"hi"} , index.html <!doctype html> ::b+a:: → "hi1"
JSON= /^\s*\{[^]*\}\s*$/gi
// relaxed Javascript Object, for use with eval([js]).pop()
function HTMX (delim){
function unpickle (__ctx, __js){ with (__ctx){ return eval (__js) }}
var
EMPTY_STR= '',
NOT_DELIM= ANY= "([^]*?)"
return (function (delim, pre){
return function renderTemplateWithContext (t, c){
var
r= t.split( new RegExp(delim.join( NOT_DELIM), 'ig')),
n= 0, s,
c= c || {}
do {
s= 3 *n + 1
if (r[s] && r[s].match( JSON)){
r[s]= unpickle( c, '[' + r[s] + ']')[0]
r[s]= pre.call( c, r[s])
r[s]= r[s].return
} else {
r[s]= unpickle( c, r[s])
}
n += 1
} while (void 0 !== r[3 *n])
return r.join(EMPTY_STR)
}
})(delim || DELIM, preprocess)
}
if (require.main === module){
if (2 >= process.argv.length)
throw new Error("use --root with --build, or --template with --context")
require('./cli')(HTMX)
} else
module.exports= HTMX

10716
src/frontend/static/js/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
src/frontend/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
{# def
title: str
#}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
{{ slot }}
</head>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<Header title="Pygentic AI">
<meta http-equiv="X-UA-Compatible" content="IE-edge, chrome=1">
<Css url="{{ url_for('static', path='/css/bulma.min.css') }}"></Css>
<Css url="{{ url_for('static', path='/css/bulma-calendar.min.css') }}"></Css>
<Css url="{{ url_for('static', path='/css/bulmatags-input.min-css') }}"></Css>
<Css url="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
</Css>
</Header>
<body>
</body>
</html>

View File

@ -0,0 +1,9 @@
{# def
url: str,
rel: str = 'stylesheet'
#}
<link rel={{ rel }}
href={{ url }}
{{ slot }}
/>

View File

@ -0,0 +1,8 @@
{# def
url: str
#}
<script
src={{ url }}
{{ slot }}
></script>