365 lines
No EOL
14 KiB
Python
365 lines
No EOL
14 KiB
Python
from flask import Flask, request, jsonify
|
|
from datetime import datetime
|
|
import os
|
|
import json
|
|
import expr
|
|
from threading import Lock
|
|
from openai import OpenAI
|
|
import logging
|
|
from flask_swagger_ui import get_swaggerui_blueprint
|
|
|
|
# Configure logging
|
|
DATA_DIR = "data"
|
|
LOG_FILE = os.path.join(DATA_DIR, "slop_with_models.log")
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Swagger UI setup
|
|
SWAGGER_URL = "/openapi"
|
|
API_URL = "/static/openapi.yaml"
|
|
swaggerui_blueprint = get_swaggerui_blueprint(
|
|
SWAGGER_URL, API_URL, config={"app_name": "SLOP API"}
|
|
)
|
|
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
|
|
|
|
# Global model-to-client map
|
|
MODEL_CLIENT_MAP = {}
|
|
|
|
# Memory setup
|
|
MEMORY_FILE = os.path.join(DATA_DIR, "memory.json")
|
|
memory_lock = Lock()
|
|
|
|
# Ensure data directory exists
|
|
if not os.path.exists(DATA_DIR):
|
|
os.makedirs(DATA_DIR)
|
|
logger.info(f"Created data directory: {DATA_DIR}")
|
|
|
|
# File-based memory functions
|
|
def load_memory_from_file():
|
|
try:
|
|
if os.path.exists(MEMORY_FILE):
|
|
with open(MEMORY_FILE, "r") as f:
|
|
data = json.load(f)
|
|
logger.debug(f"Loaded memory from {MEMORY_FILE}: {data}")
|
|
return data
|
|
logger.debug(f"No memory file found at {MEMORY_FILE}, returning empty dict")
|
|
return {}
|
|
except Exception as e:
|
|
logger.error(f"Error loading memory from {MEMORY_FILE}: {str(e)}", exc_info=True)
|
|
return {}
|
|
|
|
def save_memory_to_file(memory_data):
|
|
try:
|
|
with open(MEMORY_FILE, "w") as f:
|
|
json.dump(memory_data, f)
|
|
logger.debug(f"Saved memory to {MEMORY_FILE}: {memory_data}")
|
|
except Exception as e:
|
|
logger.error(f"Error saving memory to {MEMORY_FILE}: {str(e)}", exc_info=True)
|
|
|
|
# Load endpoints
|
|
ENDPOINTS = []
|
|
for i in range(1000):
|
|
endpoint = os.getenv(f"MODEL_ENDPOINT_{i}")
|
|
if endpoint:
|
|
api_key = os.getenv(f"MODEL_API_KEY_{i}", "not-needed")
|
|
logger.info(f"Loaded endpoint {i}: {endpoint} with API key: {api_key[:4]}...")
|
|
ENDPOINTS.append(
|
|
{
|
|
"name": f"endpoint_{i}",
|
|
"base_url": endpoint,
|
|
"api_key": api_key,
|
|
}
|
|
)
|
|
else:
|
|
logger.debug(f"No endpoint found for MODEL_ENDPOINT_{i}")
|
|
|
|
def initialize_model_map():
|
|
logger.info("Starting model map initialization")
|
|
MODEL_CLIENT_MAP.clear()
|
|
if not ENDPOINTS:
|
|
logger.warning("No endpoints configured to initialize models")
|
|
return
|
|
for ep in ENDPOINTS:
|
|
base_url = ep["base_url"]
|
|
api_key = ep["api_key"]
|
|
endpoint_name = ep["name"]
|
|
logger.info(f"Initializing client for {endpoint_name} at {base_url}")
|
|
client = OpenAI(base_url=base_url, api_key=api_key)
|
|
try:
|
|
logger.debug(f"Attempting to list models from {endpoint_name}")
|
|
response = client.models.list()
|
|
model_list = response.data
|
|
logger.debug(f"Models retrieved from {endpoint_name}: {[m.id for m in model_list]}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to list models for {endpoint_name}: {str(e)}", exc_info=True)
|
|
continue
|
|
for m in model_list:
|
|
model_id = m.id
|
|
if model_id:
|
|
if model_id in MODEL_CLIENT_MAP:
|
|
logger.warning(f"Duplicate model ID '{model_id}' found at {endpoint_name}")
|
|
else:
|
|
MODEL_CLIENT_MAP[model_id] = client
|
|
logger.info(f"Registered model '{model_id}' from {endpoint_name}")
|
|
else:
|
|
logger.warning(f"Model with no ID encountered from {endpoint_name}")
|
|
logger.info(f"Model map initialized with models: {list(MODEL_CLIENT_MAP.keys())}")
|
|
|
|
# SLOP components
|
|
tools = {
|
|
"calculator": {
|
|
"id": "calculator",
|
|
"description": "Basic math calculator",
|
|
"arguments": [
|
|
{ "name": "expression", "type": "str", "description": "the math expression to evaluate."}
|
|
],
|
|
"execute": lambda params: {"result": expr(params["expression"])},
|
|
},
|
|
"greet": {
|
|
"id": "greet",
|
|
"description": "Says hello",
|
|
"arguments": [
|
|
{ "name": "name", "type": "str", "description": "name of person to greet"}
|
|
],
|
|
"execute": lambda params: {"result": f"Hello, {params['name']}!"},
|
|
},
|
|
}
|
|
resources = {
|
|
"hello": {"id": "hello", "content": "Hello, SLOP!"},
|
|
"foo/bar": {"id": "foo/bar", "content": "Nested Foo Bar"},
|
|
"foo/baz": {"id": "foo/baz", "content": "Nested Foo Baz"},
|
|
}
|
|
|
|
# Memory endpoints with lock and timeout
|
|
LOCK_TIMEOUT = 2 # Seconds to wait for lock acquisition
|
|
|
|
@app.route("/memory", methods=["POST"])
|
|
def store_memory():
|
|
logger.info("Received /memory POST request")
|
|
data = request.json
|
|
logger.debug(f"Store memory data: {data}")
|
|
if not data or "key" not in data or "value" not in data:
|
|
logger.warning(f"Invalid memory store request: {data}")
|
|
return jsonify({"error": "Missing 'key' or 'value'"}), 400
|
|
key, value = data["key"], data["value"]
|
|
if memory_lock.acquire(timeout=LOCK_TIMEOUT):
|
|
try:
|
|
memory = load_memory_from_file() # Always load fresh state
|
|
memory[key] = value
|
|
save_memory_to_file(memory)
|
|
logger.info(f"Stored in memory: {key} = {value}")
|
|
logger.debug(f"Current memory state: {memory}")
|
|
return jsonify({"status": "stored"}), 200
|
|
finally:
|
|
memory_lock.release()
|
|
else:
|
|
logger.error(f"Failed to acquire lock for storing {key} within {LOCK_TIMEOUT} seconds")
|
|
return jsonify({"error": "Memory lock timeout"}), 503
|
|
|
|
@app.route("/memory/<key>", methods=["GET"])
|
|
def get_memory(key):
|
|
logger.info(f"Received /memory/{key} GET request")
|
|
if memory_lock.acquire(timeout=LOCK_TIMEOUT):
|
|
try:
|
|
memory = load_memory_from_file() # Always load fresh state
|
|
value = memory.get(key)
|
|
logger.debug(f"Retrieved memory for {key}: {value}")
|
|
if value is None:
|
|
logger.warning(f"Key not found in memory: {key}")
|
|
return jsonify({"value": value}), 200
|
|
finally:
|
|
memory_lock.release()
|
|
else:
|
|
logger.error(f"Failed to acquire lock for retrieving {key} within {LOCK_TIMEOUT} seconds")
|
|
return jsonify({"error": "Memory lock timeout"}), 503
|
|
|
|
@app.route("/memory", methods=["GET"])
|
|
def list_memory():
|
|
logger.info("Received /memory GET request")
|
|
if memory_lock.acquire(timeout=LOCK_TIMEOUT):
|
|
try:
|
|
memory = load_memory_from_file() # Always load fresh state
|
|
keys = list(memory.keys())
|
|
memory_state = memory.copy()
|
|
logger.debug(f"Memory keys: {keys}")
|
|
logger.debug(f"Full memory state: {memory_state}")
|
|
return jsonify({"keys": keys}), 200
|
|
finally:
|
|
memory_lock.release()
|
|
else:
|
|
logger.error(f"Failed to acquire lock for listing memory within {LOCK_TIMEOUT} seconds")
|
|
return jsonify({"error": "Memory lock timeout"}), 503
|
|
|
|
@app.route("/memory/<key>", methods=["DELETE"])
|
|
def delete_memory(key):
|
|
logger.info(f"Received /memory/{key} DELETE request")
|
|
if memory_lock.acquire(timeout=LOCK_TIMEOUT):
|
|
try:
|
|
memory = load_memory_from_file() # Always load fresh state
|
|
if key not in memory:
|
|
logger.warning(f"Key not found for deletion: {key}")
|
|
return jsonify({"error": "Key not found"}), 404
|
|
old_value = memory[key]
|
|
del memory[key]
|
|
save_memory_to_file(memory)
|
|
logger.info(f"Deleted from memory: {key} (was {old_value})")
|
|
logger.debug(f"Updated memory state: {memory}")
|
|
return jsonify({"status": "deleted"}), 200
|
|
finally:
|
|
memory_lock.release()
|
|
else:
|
|
logger.error(f"Failed to acquire lock for deleting {key} within {LOCK_TIMEOUT} seconds")
|
|
return jsonify({"error": "Memory lock timeout"}), 503
|
|
|
|
# Other endpoints (unchanged for brevity)
|
|
@app.route("/chat", methods=["POST"])
|
|
def chat():
|
|
logger.info("Received /chat request")
|
|
data = request.json
|
|
logger.debug(f"Request data: {data}")
|
|
message = data["messages"][0]["content"] if data.get("messages") else "nothing"
|
|
model_id = data.get("model") or (
|
|
list(MODEL_CLIENT_MAP.keys())[0] if MODEL_CLIENT_MAP else None
|
|
)
|
|
logger.info(f"Selected model: {model_id}, message: {message}")
|
|
if not model_id or model_id not in MODEL_CLIENT_MAP:
|
|
logger.error(f"Invalid or missing model_id: {model_id}")
|
|
return jsonify({"error": "Model not found"}), 404
|
|
client = MODEL_CLIENT_MAP[model_id]
|
|
try:
|
|
response = client.chat.completions.create(
|
|
model=model_id,
|
|
messages=[
|
|
{"role": m["role"], "content": m["content"]}
|
|
for m in data.get("messages", [])
|
|
]
|
|
or [{"role": "user", "content": message}],
|
|
)
|
|
response_content = response.choices[0].message.content
|
|
logger.debug(f"Chat response from {model_id}: {response_content}")
|
|
return (
|
|
jsonify(
|
|
{
|
|
"choices": [
|
|
{"message": {"content": response_content}}
|
|
]
|
|
}
|
|
),
|
|
200,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Chat error with model {model_id}: {str(e)}", exc_info=True)
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route("/models", methods=["GET"])
|
|
def list_models():
|
|
logger.info("Received /models request")
|
|
models = list(MODEL_CLIENT_MAP.keys())
|
|
logger.debug(f"Returning models: {models}")
|
|
return jsonify({"models": models}), 200
|
|
|
|
@app.route("/tools", methods=["GET"])
|
|
def list_tools():
|
|
logger.info("Received /tools request")
|
|
tool_list = [{"id": k, "description": v["description"], "arguments": v.get("arguments", [])} for k, v in tools.items()]
|
|
logger.debug(f"Returning tools: {tool_list}")
|
|
return jsonify({"tools": tool_list}), 200
|
|
|
|
@app.route("/tools/<tool_id>", methods=["POST"])
|
|
def use_tool(tool_id):
|
|
logger.info(f"Received /tools/{tool_id} request")
|
|
if tool_id not in tools:
|
|
logger.error(f"Tool not found: {tool_id}")
|
|
return jsonify({"error": "Tool not found"}), 404
|
|
data = request.json or {}
|
|
logger.debug(f"Tool {tool_id} input data: {data}")
|
|
|
|
if "arguments" in tools[tool_id]:
|
|
for arg in tools[tool_id]["arguments"]:
|
|
if arg["name"] not in data:
|
|
logger.warning(f"Missing '{arg['name']}' for {tool_id} tool")
|
|
return jsonify({"error": f"Missing '{arg['name']}' parameter"}), 400
|
|
|
|
try:
|
|
result = tools[tool_id]["execute"](data)
|
|
logger.debug(f"Tool {tool_id} result: {result}")
|
|
return jsonify(result), 200
|
|
except Exception as e:
|
|
logger.error(f"Error executing tool {tool_id}: {str(e)}", exc_info=True)
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route("/resources", methods=["GET"])
|
|
def list_resources():
|
|
logger.info("Received /resources request")
|
|
resource_list = list(resources.values())
|
|
logger.debug(f"Returning resources: {resource_list}")
|
|
return jsonify({"resources": resource_list}), 200
|
|
|
|
@app.route("/resources/<resource_id>", methods=["GET"])
|
|
def get_resource(resource_id):
|
|
logger.info(f"Received /resources/{resource_id} GET request")
|
|
if resource_id in resources:
|
|
resource = resources[resource_id]
|
|
logger.debug(f"Exact match found for {resource_id}: {resource}")
|
|
return jsonify(resource), 200
|
|
else:
|
|
# Prefix search for nested resources
|
|
prefix = f"{resource_id}/"
|
|
matching_resources = [
|
|
res for key, res in resources.items() if key.startswith(prefix)
|
|
]
|
|
if matching_resources:
|
|
logger.debug(f"Prefix search for {resource_id} found: {matching_resources}")
|
|
return jsonify({"resources": matching_resources}), 200
|
|
logger.warning(f"No resource or nested resources found for {resource_id}")
|
|
return jsonify({"error": "Resource not found"}), 404
|
|
|
|
@app.route("/resources/<resource_id>", methods=["PUT"])
|
|
def update_resource(resource_id):
|
|
logger.info(f"Received /resources/{resource_id} PUT request")
|
|
data = request.json
|
|
logger.debug(f"Update resource data: {data}")
|
|
if not data or "content" not in data:
|
|
logger.warning(f"Invalid resource update request: {data}")
|
|
return jsonify({"error": "Missing 'content'"}), 400
|
|
# Update or create the resource
|
|
resources[resource_id] = {
|
|
"id": resource_id,
|
|
"content": data["content"]
|
|
}
|
|
logger.info(f"Updated/created resource {resource_id}: {resources[resource_id]}")
|
|
return jsonify({"status": "updated", "resource": resources[resource_id]}), 200
|
|
|
|
@app.route("/pay", methods=["POST"])
|
|
def pay():
|
|
logger.info("Received /pay request")
|
|
data = request.json
|
|
logger.debug(f"Pay request data: {data}")
|
|
amount = data.get("amount", 0)
|
|
transaction_id = f"tx_{int(datetime.now().timestamp())}"
|
|
logger.info(f"Processed payment of {amount} with transaction ID: {transaction_id}")
|
|
return (
|
|
jsonify(
|
|
{
|
|
"transaction_id": transaction_id,
|
|
"status": "success",
|
|
}
|
|
),
|
|
200,
|
|
)
|
|
|
|
# Initialize model map on startup
|
|
logger.info("Starting application initialization")
|
|
initialize_model_map()
|
|
logger.info("Application initialization completed")
|
|
|
|
if __name__ == "__main__":
|
|
logger.info("Starting Flask application on port 31337")
|
|
app.run(debug=True, port=31337)
|
|
logger.info("Flask application stopped") |