This commit is contained in:
codey 2024-11-14 14:37:32 -05:00
parent ed5eb36ebb
commit 2110914133
6 changed files with 309 additions and 89 deletions

24
base.py
View File

@ -5,15 +5,13 @@ import logging
import asyncio import asyncio
from typing import Any from typing import Any
from fastapi import FastAPI, WebSocket from fastapi import FastAPI
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi_utils.tasks import repeat_every from fastapi_utils.tasks import repeat_every
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.DEBUG) logger.setLevel(logging.CRITICAL)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
app = FastAPI(title="codey.lol API", app = FastAPI(title="codey.lol API",
@ -27,6 +25,7 @@ app.loop = loop
constants = importlib.import_module("constants").Constants() constants = importlib.import_module("constants").Constants()
util = importlib.import_module("util").Utilities(app, constants) util = importlib.import_module("util").Utilities(app, constants)
glob_state = importlib.import_module("state").State(app, util, constants) glob_state = importlib.import_module("state").State(app, util, constants)
@ -60,6 +59,10 @@ def disallow_get_any(var: Any = None):
def disallow_base_post(): def disallow_base_post():
return util.get_blocked_response() return util.get_blocked_response()
# @app.limiter.limit("1/minute")
# @app.post("/lyric_cache_list/")
# async def rate_limited():
# return {"error": "Rate limited"}
""" """
End Blacklisted Routes End Blacklisted Routes
@ -82,12 +85,15 @@ yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants,
# Below: XC endpoint(s) # Below: XC endpoint(s)
xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants, glob_state) xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants, glob_state)
# Below: CAH endpoint(s) # Below: CAH endpoint(s)
cah_endpoints = importlib.import_module("endpoints.cah").CAH(app, util, constants, glob_state) # cah_endpoints = importlib.import_module("endpoints.cah").CAH(app, util, constants, glob_state)
@app.on_event("startup") # Below: Karma endpoint(s)
@repeat_every(seconds=10) karma_endpoints = importlib.import_module("endpoints.karma").Karma(app, util, constants, glob_state)
async def cah_tasks() -> None:
return await cah_endpoints.periodicals() # @app.on_event("startup")
# @repeat_every(seconds=10)
# async def cah_tasks() -> None:
# return await cah_endpoints.periodicals()

View File

@ -27,8 +27,8 @@ class AI(FastAPI):
self.glob_state = glob_state self.glob_state = glob_state
self.url_clean_regex = regex.compile(r'^\/ai\/(openai|base)\/') self.url_clean_regex = regex.compile(r'^\/ai\/(openai|base)\/')
self.endpoints = { self.endpoints = {
# "ai/openai": self.ai_openai_handler, "ai/openai": self.ai_openai_handler,
# "ai/base": self.ai_handler, "ai/base": self.ai_handler,
"ai/song": self.ai_song_handler "ai/song": self.ai_song_handler
#tbd #tbd
} }
@ -36,70 +36,74 @@ class AI(FastAPI):
for endpoint, handler in self.endpoints.items(): for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/{{any:path}}", handler, methods=["POST"]) app.add_api_route(f"/{endpoint}/{{any:path}}", handler, methods=["POST"])
# async def ai_handler(self, request: Request): async def ai_handler(self, request: Request):
# """ """
# /ai/base/ /ai/base/
# AI BASE Request AI BASE Request
# """ """
# if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')): if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
# raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
# local_llm_headers = { local_llm_headers = {
# 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}' 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}'
# } }
# forward_path = self.url_clean_regex.sub('', request.url.path) forward_path = self.url_clean_regex.sub('', request.url.path)
# try: try:
# async with ClientSession() as session: async with ClientSession() as session:
# async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/{forward_path}', async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/{forward_path}',
# json=await request.json(), json=await request.json(),
# headers=local_llm_headers, headers=local_llm_headers,
# timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: timeout=ClientTimeout(connect=15, sock_read=30)) as out_request:
# await self.glob_state.increment_counter('ai_requests') await self.glob_state.increment_counter('ai_requests')
# response = await out_request.json() response = await out_request.json()
# return response return response
# except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
# logging.error("Error: %s", e) logging.error("Error: %s", e)
# return { return {
# 'err': True, 'err': True,
# 'errorText': 'General Failure' 'errorText': 'General Failure'
# } }
# async def ai_openai_handler(self, request: Request): async def ai_openai_handler(self, request: Request):
# """ """
# /ai/openai/ /ai/openai/
# AI Request AI Request
# """ """
# if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')): if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
# raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
# """ """
# TODO: Implement Claude TODO: Implement Claude
# Currently only routes to local LLM Currently only routes to local LLM
# """ """
# local_llm_headers = { local_llm_headers = {
# 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}' 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}'
# } }
# forward_path = self.url_clean_regex.sub('', request.url.path) forward_path = self.url_clean_regex.sub('', request.url.path)
# try: try:
# async with ClientSession() as session: async with ClientSession() as session:
# async with await session.post(f'{self.constants.LOCAL_LLM_HOST}/{forward_path}', async with await session.post(f'{self.constants.LOCAL_LLM_HOST}/{forward_path}',
# json=await request.json(), json=await request.json(),
# headers=local_llm_headers, headers=local_llm_headers,
# timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: timeout=ClientTimeout(connect=15, sock_read=30)) as out_request:
# await self.glob_state.increment_counter('ai_requests') await self.glob_state.increment_counter('ai_requests')
# response = await out_request.json() response = await out_request.json()
# return response return response
# except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
# logging.error("Error: %s", e) logging.error("Error: %s", e)
# return { return {
# 'err': True, 'err': True,
# 'errorText': 'General Failure' 'errorText': 'General Failure'
# } }
"""
CLAUDE BELOW, COMMENTED
"""
async def ai_song_handler(self, data: ValidAISongRequest, request: Request): async def ai_song_handler(self, data: ValidAISongRequest, request: Request):
""" """
@ -114,7 +118,7 @@ class AI(FastAPI):
local_llm_headers = { local_llm_headers = {
'x-api-key': self.constants.LOCAL_LLM_KEY, 'x-api-key': self.constants.CLAUDE_API_KEY,
'anthropic-version': '2023-06-01', 'anthropic-version': '2023-06-01',
'content-type': 'application/json', 'content-type': 'application/json',
} }
@ -132,18 +136,6 @@ class AI(FastAPI):
] ]
} }
# ai_req_data = {
# 'max_context_length': 16784,
# 'max_length': 256,
# 'temperature': 0.2,
# 'quiet': 1,
# 'bypass_eos': False,
# 'trim_stop': True,
# 'sampler_order': [6,0,1,3,4,2,5],
# 'memory': "You are a helpful assistant who will provide only totally accurate tidbits of info on songs the user may listen to. You do not include information about which album a song was released on, or when it was released, and do not mention that you are not including this information in your response. If the input provided is not a song you are aware of, simply state that.",
# 'stop': ['### Inst', '### Resp'],
# 'prompt': ai_question
# }
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post('https://api.anthropic.com/v1/messages', async with await session.post('https://api.anthropic.com/v1/messages',
@ -163,3 +155,45 @@ class AI(FastAPI):
'err': True, 'err': True,
'errorText': 'General Failure' 'errorText': 'General Failure'
} }
# async def ai_song_handler(self, data: ValidAISongRequest, request: Request):
# """
# /ai/song/
# AI (Song Info) Request [Public]
# """
# ai_question = f"I am going to listen to the song \"{data.s}\" by \"{data.a}\"."
# local_llm_headers = {
# 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}'
# }
# ai_req_data = {
# 'max_context_length': 8192,
# 'max_length': 256,
# 'temperature': 0.3,
# 'quiet': 1,
# 'bypass_eos': False,
# 'trim_stop': True,
# 'sampler_order': [6,0,1,3,4,2,5],
# 'memory': "You are a helpful assistant who will provide only totally accurate tidbits of info on songs the user may listen to. You do not include information about which album a song was released on, or when it was released, and do not mention that you are not including this information in your response. If the input provided is not a song you are aware of, simply state that. Begin your output at your own response.",
# 'stop': ['### Inst', '### Resp'],
# 'prompt': ai_question
# }
# try:
# async with ClientSession() as session:
# async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/generate',
# json=ai_req_data,
# headers=local_llm_headers,
# timeout=ClientTimeout(connect=15, sock_read=30)) as request:
# await self.glob_state.increment_counter('ai_requests')
# response = await request.json()
# result = {
# 'resp': response.get('results')[0].get('text').strip()
# }
# return result
# except Exception as e: # pylint: disable=broad-exception-caught
# logging.error("Error: %s", e)
# return {
# 'err': True,
# 'errorText': 'General Failure'
# }

View File

@ -21,7 +21,6 @@ class CAH(FastAPI):
async def send_heartbeats(self) -> None: async def send_heartbeats(self) -> None:
try: try:
while True: while True:
logging.critical("Heartbeat!")
self.connection_manager.broadcast({ self.connection_manager.broadcast({
"event": "heartbeat", "event": "heartbeat",
"ts": int(time.time()) "ts": int(time.time())
@ -33,7 +32,6 @@ class CAH(FastAPI):
async def clean_stale_games(self) -> None: async def clean_stale_games(self) -> None:
try: try:
logging.critical("Looking for stale games...")
for game in self.games: for game in self.games:
print(f"Checking {game}...") print(f"Checking {game}...")
if not game.players: if not game.players:

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3.12 #!/usr/bin/env python3.12
#!/usr/bin/env python3.12
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel

168
endpoints/karma.py Normal file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3.12
import os
import time
import datetime
import aiosqlite as sqlite3
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
class ValidKarmaUpdateRequest(BaseModel):
"""
Requires authentication
- **granter**: who updated the karma
- **keyword**: keyword to update karma for
- **flag**: either 0 (decrement) for --, or 1 (increment) for ++
"""
granter: str
keyword: str
flag: int
class ValidKarmaRetrievalRequest(BaseModel):
"""
- **keyword**: keyword to retrieve karma value of
"""
keyword: str
class KarmaDB:
def __init__(self):
self.db_path = os.path.join("/", "var", "lib", "singerdbs", "karma.db")
async def get_karma(self, keyword: str) -> str | int:
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with db_conn.execute("SELECT score FROM karma WHERE keyword = ? LIMIT 1", (keyword,)) as db_cursor:
try:
(score,) = await db_cursor.fetchone()
return score
except TypeError as e:
return {
'err': True,
'errorText': f'No records for {keyword}',
}
async def get_top_10(self):
try:
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with db_conn.execute("SELECT keyword, score FROM karma ORDER BY score DESC LIMIT 10") as db_cursor:
return await db_cursor.fetchall()
except Exception as e:
print(traceback.format_exc())
return
async def update_karma(self, granter: str, keyword: str, flag: int):
if not flag in [0, 1]:
return
modifier = "score + 1" if not flag else "score - 1"
query = f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword = ?"
new_keyword_query = "INSERT INTO karma(keyword, score, last_change) VALUES(?, ?, ?)"
friendly_flag = "++" if not flag else "--"
audit_message = f"{granter} adjusted karma for {keyword} @ {datetime.datetime.now().isoformat()}: {friendly_flag}"
audit_query = "INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
now = int(time.time())
print(f"Audit message: {audit_message}\nKeyword: {keyword}")
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with db_conn.execute(audit_query, (keyword, audit_message,)) as db_cursor:
await db_conn.commit()
await db_cursor.close()
async with db_conn.execute(query, (now, keyword,)) as db_cursor:
if db_cursor.rowcount:
await db_conn.commit()
return True
if db_cursor.rowcount < 1: # Keyword does not already exist
await db_cursor.close()
new_val = 1 if not flag else -1
async with db_conn.execute(new_keyword_query, (keyword, new_val, now,)) as db_cursor:
if db_cursor.rowcount >= 1:
await db_conn.commit()
return True
else:
return False
class Karma(FastAPI):
"""Karma Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called
self.app = app
self.util = util
self.constants = constants
self.glob_state = glob_state
self.db = KarmaDB()
self.endpoints = {
"karma/get": self.get_karma_handler,
"karma/modify": self.modify_karma_handler,
"karma/top": self.top_karma_handler,
}
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
async def top_karma_handler(self):
"""
/karma/top/
Get top 10 for karma
"""
try:
top10 = await self.db.get_top_10()
return top10
except Exception as e:
print(traceback.format_exc())
return {
'err': True,
'errorText': 'Exception occurred.',
}
async def get_karma_handler(self, data: ValidKarmaRetrievalRequest):
"""
/karma/get/
Get current karma value
"""
keyword = data.keyword
try:
count = await self.db.get_karma(keyword)
return {
'keyword': keyword,
'count': count,
}
except:
print(traceback.format_exc())
return {
'err': True,
'errorText': "Exception occurred."
}
async def modify_karma_handler(self, data: ValidKarmaUpdateRequest, request: Request):
"""
/karma/update/
Update karma count (requires PUT KEY)
"""
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With'), 2):
raise HTTPException(status_code=403, detail="Unauthorized")
if not data.flag in [0, 1]:
return {
'err': True,
'errorText': 'Invalid request'
}
return {
'success': await self.db.update_karma(data.granter, data.keyword, data.flag)
}

View File

@ -2,10 +2,19 @@
import os import os
import random import random
import traceback
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from fastapi import FastAPI from typing import Optional
from fastapi import FastAPI, Request
from pydantic import BaseModel
class RandMsgRequest(BaseModel):
"""
- **short**: Short randmsg?
"""
short: Optional[bool] = False
class RandMsg(FastAPI): class RandMsg(FastAPI):
"""Random Message Endpoint""" """Random Message Endpoint"""
@ -19,12 +28,16 @@ class RandMsg(FastAPI):
app.add_api_route(f"/{self.endpoint_name}/", self.randmsg_handler, methods=["POST"]) app.add_api_route(f"/{self.endpoint_name}/", self.randmsg_handler, methods=["POST"])
async def randmsg_handler(self): async def randmsg_handler(self, data: RandMsgRequest = None):
""" """
Get a randomly generated message Get a randomly generated message
""" """
random.seed() random.seed()
db_rand_selected = random.choice([0, 1, 3]) short = data.short if data else False
if not short:
db_rand_selected = random.choice([0, 1, 3])
else:
db_rand_selected = 9
title_attr = "Unknown" title_attr = "Unknown"
match db_rand_selected: match db_rand_selected:
@ -37,7 +50,7 @@ class RandMsg(FastAPI):
db_query = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \ db_query = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db || answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
title_attr = "QA Joke DB" title_attr = "QA Joke DB"
case 1: case 1 | 9:
randmsg_db_path = os.path.join("/", randmsg_db_path = os.path.join("/",
"var", "var",
"lib", "lib",
@ -45,6 +58,8 @@ class RandMsg(FastAPI):
"randmsg.db") # For randmsg db "randmsg.db") # For randmsg db
db_query = "SELECT id, msg FROM msgs WHERE \ db_query = "SELECT id, msg FROM msgs WHERE \
LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db
if db_rand_selected == 9:
db_query = db_query.replace("<= 180", "<= 126")
title_attr = "Random Msg DB" title_attr = "Random Msg DB"
case 2: case 2:
randmsg_db_path = os.path.join("/", randmsg_db_path = os.path.join("/",