forked from retoor/devplacepy
feat: add ISSLOP AI usage analysis CLI commands, game router, and politics topic
- Add `cmd_isslop_prune`, `cmd_isslop_clear`, `cmd_isslop_analyze` CLI commands for AI usage analysis job management - Register `/game` router with `index` and `farm` endpoints for Code Farm idle game - Add `politics` to allowed TOPICS constant replacing `signals` - Introduce `ISSLOP_DIR`, `ISSLOP_WORKSPACES_DIR`, `ISSLOP_RUNS_DIR`, `ISSLOP_MEDIA_DIR` config paths - Add `clear_user_stars` and `clear_user_projects_cache` calls on vote and project create/delete - Update `make prod` to use `nproc` workers via `DEVPLACE_WEB_WORKERS` env var - Convert `database.py` and `utils.py` to packages for modular structure - Add `devplace apikey` and `devplace token` CLI subcommands for API key and access token management
This commit is contained in:
@@ -32,6 +32,7 @@ CATEGORY_BY_PREFIX: dict[str, str] = {
|
||||
"proxy": "ingress",
|
||||
"seo": "tools",
|
||||
"deepsearch": "tools",
|
||||
"isslop": "tools",
|
||||
"ai": "ai",
|
||||
"database": "database",
|
||||
"pubsub": "pubsub",
|
||||
|
||||
@@ -102,7 +102,7 @@ REACT_RATES = {
|
||||
}
|
||||
REACT_RATE_DEFAULT = 0.20
|
||||
|
||||
CATEGORIES = ["devlog", "showcase", "question", "rant", "fun", "random"]
|
||||
CATEGORIES = ["devlog", "showcase", "question", "rant", "fun", "random", "politics"]
|
||||
|
||||
GIST_LANGUAGES = [
|
||||
"python",
|
||||
@@ -122,7 +122,7 @@ GIST_LANGUAGES = [
|
||||
"lua",
|
||||
]
|
||||
|
||||
FEED_TOPICS = ["devlog", "showcase", "question", "rant", "fun"]
|
||||
FEED_TOPICS = ["devlog", "showcase", "question", "rant", "fun", "politics"]
|
||||
PROJECT_TYPES = ["game", "game_asset", "software", "mobile_app", "website"]
|
||||
|
||||
SEARCH_TERMS = {
|
||||
@@ -240,6 +240,7 @@ PERSONA_CATEGORY_WEIGHTS = {
|
||||
"question": 1,
|
||||
"showcase": 1,
|
||||
"fun": 1,
|
||||
"politics": 1,
|
||||
},
|
||||
"hobbyist_maker": {
|
||||
"showcase": 3,
|
||||
@@ -256,6 +257,7 @@ PERSONA_CATEGORY_WEIGHTS = {
|
||||
"showcase": 1,
|
||||
"rant": 1,
|
||||
"fun": 1,
|
||||
"politics": 1,
|
||||
},
|
||||
"minimalist": {
|
||||
"random": 3,
|
||||
@@ -280,6 +282,7 @@ PERSONA_CATEGORY_WEIGHTS = {
|
||||
"random": 1,
|
||||
"showcase": 1,
|
||||
"devlog": 1,
|
||||
"politics": 2,
|
||||
},
|
||||
"mentor": {
|
||||
"devlog": 3,
|
||||
@@ -288,6 +291,7 @@ PERSONA_CATEGORY_WEIGHTS = {
|
||||
"random": 1,
|
||||
"rant": 1,
|
||||
"fun": 1,
|
||||
"politics": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ class LLMClient:
|
||||
"rant": "Write it as an opinionated rant - strong viewpoint, passionate criticism, but keep it substantive.",
|
||||
"fun": "Write it lighthearted and playful, but stay tied to the actual tech topic. Humor about the technology itself, not off-topic jokes or lyrics.",
|
||||
"random": "Be natural and conversational - share your thoughts like any casual discussion.",
|
||||
"politics": "Write it as a measured take on the politics of this technology - regulation, governance, open-source licensing, industry power, or ethics. Stay substantive and non-partisan; argue the policy angle, not party lines.",
|
||||
}
|
||||
persona_extra = (
|
||||
f" {persona_extras.get(persona, 'Be casual. No markdown.')}"
|
||||
|
||||
@@ -415,13 +415,13 @@ def pravda_env(instance: dict) -> dict:
|
||||
slug = instance.get("ingress_slug") or ""
|
||||
ingress_url = (f"{base_url}/p/{slug}" if base_url else f"/p/{slug}") if slug else ""
|
||||
return {
|
||||
"PRAVDA_BASE_URL": base_url,
|
||||
"PRAVDA_OPENAI_URL": f"{base_url}/openai/v1" if base_url else "",
|
||||
"PRAVDA_API_KEY": api_key,
|
||||
"PRAVDA_USER_UID": instance.get("run_as_uid") or user_uid,
|
||||
"PRAVDA_CONTAINER_NAME": instance.get("name") or "",
|
||||
"PRAVDA_CONTAINER_UID": instance.get("uid") or "",
|
||||
"PRAVDA_INGRESS_URL": ingress_url,
|
||||
"DEVPLACE_BASE_URL": base_url,
|
||||
"DEVPLACE_OPENAI_URL": f"{base_url}/openai/v1" if base_url else "",
|
||||
"DEVPLACE_API_KEY": api_key,
|
||||
"DEVPLACE_USER_UID": instance.get("run_as_uid") or user_uid,
|
||||
"DEVPLACE_CONTAINER_NAME": instance.get("name") or "",
|
||||
"DEVPLACE_CONTAINER_UID": instance.get("uid") or "",
|
||||
"DEVPLACE_INGRESS_URL": ingress_url,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
" retoor <retoor@molodetz.nl>
|
||||
" Self-contained config for the ppy container. No external plugins or managers:
|
||||
" it works out of the box with the stock vim, and the AI edit feature uses only
|
||||
" curl and the PRAVDA_* gateway env that every instance is launched with.
|
||||
" curl and the DEVPLACE_* gateway env that every instance is launched with.
|
||||
|
||||
set nocompatible
|
||||
filetype plugin indent on
|
||||
@@ -70,7 +70,7 @@ function! s:GetVisualSelection() abort
|
||||
endfunction
|
||||
|
||||
function! s:GatewayUrl() abort
|
||||
let l:base = substitute($PRAVDA_OPENAI_URL, '/\+$', '', '')
|
||||
let l:base = substitute($DEVPLACE_OPENAI_URL, '/\+$', '', '')
|
||||
if empty(l:base)
|
||||
return 'https://openai.app.molodetz.nl/v1/chat/completions'
|
||||
elseif l:base =~# '/chat/completions$'
|
||||
@@ -92,7 +92,7 @@ function! AiEditSelection() abort
|
||||
endif
|
||||
let l:prompt = l:instruction . "\n\nHere is the text:\n" . l:orig
|
||||
\ . "\n\nOutput only the transformed text. No explanations, markdown, or code blocks."
|
||||
let l:api_key = !empty($PRAVDA_API_KEY) ? $PRAVDA_API_KEY : $DEEPSEEK_API_KEY
|
||||
let l:api_key = !empty($DEVPLACE_API_KEY) ? $DEVPLACE_API_KEY : $DEEPSEEK_API_KEY
|
||||
let l:json = '{"model":"deepseek-chat","messages":[{"role":"user","content":' . json_encode(l:prompt) . '}]}'
|
||||
let l:cmd = 'curl -sS -X POST ' . shellescape(s:GatewayUrl())
|
||||
\ . ' -H ' . shellescape('Authorization: Bearer ' . l:api_key)
|
||||
|
||||
@@ -49,14 +49,14 @@ from urllib.parse import quote, unquote, urlencode, urlparse, urlsplit, urlunspl
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _resolve_devplace_url() -> str:
|
||||
base = os.environ.get("PRAVDA_BASE_URL", "").strip().rstrip("/")
|
||||
base = os.environ.get("DEVPLACE_BASE_URL", "").strip().rstrip("/")
|
||||
if base:
|
||||
return base
|
||||
return os.environ.get("DEVPLACE_URL", "https://devplace.net").strip().rstrip("/")
|
||||
|
||||
|
||||
def _resolve_llm_endpoint() -> str:
|
||||
base = os.environ.get("PRAVDA_OPENAI_URL", "").strip().rstrip("/")
|
||||
base = os.environ.get("DEVPLACE_OPENAI_URL", "").strip().rstrip("/")
|
||||
if not base:
|
||||
base = "https://openai.app.molodetz.nl/v1"
|
||||
return base if base.endswith("/chat/completions") else base + "/chat/completions"
|
||||
@@ -64,7 +64,7 @@ def _resolve_llm_endpoint() -> str:
|
||||
|
||||
DEVPLACE_URL = _resolve_devplace_url()
|
||||
DEVPLACE_API_KEY = (
|
||||
os.environ.get("PRAVDA_API_KEY")
|
||||
os.environ.get("DEVPLACE_API_KEY")
|
||||
or os.environ.get("DEVPLACE_API_KEY")
|
||||
or "019ea58c-fae0-7112-8025-e629a54104a4"
|
||||
)
|
||||
@@ -81,7 +81,7 @@ LLM_ENDPOINT = _resolve_llm_endpoint()
|
||||
LLM_BASE_URL = LLM_ENDPOINT.rsplit("/chat/completions", 1)[0]
|
||||
MODEL = "molodetz"
|
||||
LLM_API_KEY = str(
|
||||
os.environ.get("PRAVDA_API_KEY")
|
||||
os.environ.get("DEVPLACE_API_KEY")
|
||||
or os.environ.get("LLM_API_KEY")
|
||||
or ""
|
||||
)
|
||||
|
||||
@@ -659,7 +659,7 @@ from typing import Any, Callable, Optional
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
def _resolve_llm_endpoint() -> str:
|
||||
base = os.environ.get("PRAVDA_OPENAI_URL", "").strip().rstrip("/")
|
||||
base = os.environ.get("DEVPLACE_OPENAI_URL", "").strip().rstrip("/")
|
||||
if not base:
|
||||
base = "https://openai.app.molodetz.nl/v1"
|
||||
return base if base.endswith("/chat/completions") else base + "/chat/completions"
|
||||
@@ -669,7 +669,7 @@ LLM_ENDPOINT = _resolve_llm_endpoint()
|
||||
LLM_BASE_URL = LLM_ENDPOINT.rsplit("/chat/completions", 1)[0]
|
||||
MODEL = "molodetz"
|
||||
API_KEY = (
|
||||
os.environ.get("PRAVDA_API_KEY")
|
||||
os.environ.get("DEVPLACE_API_KEY")
|
||||
or os.environ.get("LLM_API_KEY")
|
||||
or str(uuid.uuid4())
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -41,14 +41,14 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode, urljoin, urlparse
|
||||
|
||||
def _resolve_api_url() -> str:
|
||||
base = os.environ.get("PRAVDA_OPENAI_URL", "").strip().rstrip("/")
|
||||
base = os.environ.get("DEVPLACE_OPENAI_URL", "").strip().rstrip("/")
|
||||
if not base:
|
||||
return "https://openai.app.molodetz.nl/v1/chat/completions"
|
||||
return base if base.endswith("/chat/completions") else base + "/chat/completions"
|
||||
|
||||
|
||||
API_URL = _resolve_api_url()
|
||||
API_KEY = os.environ.get("PRAVDA_API_KEY") or os.environ.get("DEEPSEEK_API_KEY") or ""
|
||||
API_KEY = os.environ.get("DEVPLACE_API_KEY") or os.environ.get("DEEPSEEK_API_KEY") or ""
|
||||
RSEARCH_BASE_URL = "https://rsearch.app.molodetz.nl"
|
||||
RSEARCH_MODES = ("search", "chat", "describe", "health")
|
||||
RSEARCH_MAX_BYTES = 8 * 1024 * 1024
|
||||
@@ -1644,7 +1644,7 @@ async def describe_image(prompt: str, image_path: str):
|
||||
|
||||
api_key = API_KEY
|
||||
if not api_key:
|
||||
return json.dumps({"status": "error", "error": "PRAVDA_API_KEY or DEEPSEEK_API_KEY missing"})
|
||||
return json.dumps({"status": "error", "error": "DEVPLACE_API_KEY or DEEPSEEK_API_KEY missing"})
|
||||
|
||||
payload = {
|
||||
"model": DEFAULT_MODEL,
|
||||
@@ -1811,7 +1811,7 @@ async def delegate(task: str, allowed_tools: Optional[list] = None):
|
||||
"""
|
||||
api_key = API_KEY
|
||||
if not api_key:
|
||||
return json.dumps({"status": "error", "error": "PRAVDA_API_KEY or DEEPSEEK_API_KEY missing"})
|
||||
return json.dumps({"status": "error", "error": "DEVPLACE_API_KEY or DEEPSEEK_API_KEY missing"})
|
||||
if allowed_tools:
|
||||
sub_payloads = [
|
||||
t for t in get_tool_payloads()
|
||||
@@ -2567,7 +2567,7 @@ async def amain(headed: bool = False, prompt: str = "", timeout: float = 0) -> N
|
||||
|
||||
api_key = API_KEY
|
||||
if not api_key:
|
||||
md.print("**Error:** `PRAVDA_API_KEY` or `DEEPSEEK_API_KEY` missing.")
|
||||
md.print("**Error:** `DEVPLACE_API_KEY` or `DEEPSEEK_API_KEY` missing.")
|
||||
sys.exit(1)
|
||||
|
||||
clone_mode = os.environ.get("MAK_CLONE_MODE") == "1"
|
||||
|
||||
@@ -108,4 +108,58 @@ TOOLS_ACTIONS: tuple[Action, ...] = (
|
||||
params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
|
||||
requires_auth=False,
|
||||
),
|
||||
Action(
|
||||
name="isslop",
|
||||
method="POST",
|
||||
path="/tools/isslop/run",
|
||||
summary="Classify a repository or website as AI slop or human work",
|
||||
description=(
|
||||
"Queues a background AI Usage Analyzer job and returns {uid, status_url, report_url}. "
|
||||
"Poll the status with isslop_status until status is 'completed', then share the "
|
||||
"authenticity grade, category, human/AI split and report_url. Accepts http(s), "
|
||||
"git and ssh source URLs."
|
||||
),
|
||||
params=(
|
||||
body("url", "Repository or website URL to classify.", required=True),
|
||||
),
|
||||
requires_auth=True,
|
||||
),
|
||||
Action(
|
||||
name="isslop_status",
|
||||
method="GET",
|
||||
path="/tools/isslop/{uid}",
|
||||
summary="Check a AI usage analysis and obtain its verdict once finished",
|
||||
description=(
|
||||
"Returns the analysis status. When status is 'completed', grade, category, "
|
||||
"human_percent, ai_percent and report_url are populated; while 'pending' or "
|
||||
"'running', poll again shortly."
|
||||
),
|
||||
params=(path("uid", "Analysis uid returned by isslop."),),
|
||||
requires_auth=True,
|
||||
),
|
||||
Action(
|
||||
name="isslop_report",
|
||||
method="GET",
|
||||
path="/tools/isslop/{uid}/report",
|
||||
summary="Read a finished AI usage analysis report",
|
||||
description=(
|
||||
"Returns the full report for a finished analysis: authenticity grade, human/AI "
|
||||
"split, markdown findings, per-file scores with signals, the image review and the "
|
||||
"embeddable badge snippets. Use it after isslop_status reports status 'completed'."
|
||||
),
|
||||
params=(path("uid", "Analysis uid returned by isslop."),),
|
||||
requires_auth=True,
|
||||
),
|
||||
Action(
|
||||
name="isslop_list",
|
||||
method="GET",
|
||||
path="/tools/isslop/list",
|
||||
summary="List the user's AI usage analyses",
|
||||
description=(
|
||||
"Returns the signed-in user's analysis history, newest first, each with its grade, "
|
||||
"category, status and report_url."
|
||||
),
|
||||
params=(),
|
||||
requires_auth=True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
arg(
|
||||
"run_as_uid",
|
||||
"Optional DevPlace user uid whose identity and API key are injected (PRAVDA_API_KEY, PRAVDA_USER_UID). Does NOT change the container OS user, which is always pravda (uid 1000).",
|
||||
"Optional DevPlace user uid whose identity and API key are injected (DEVPLACE_API_KEY, DEVPLACE_USER_UID). Does NOT change the container OS user, which is always pravda (uid 1000).",
|
||||
),
|
||||
arg(
|
||||
"start_on_boot",
|
||||
@@ -118,7 +118,7 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
|
||||
arg("instance", "Instance name, slug, or uid.", required=True),
|
||||
arg(
|
||||
"run_as_uid",
|
||||
"DevPlace user uid whose identity and API key are injected (PRAVDA_API_KEY, PRAVDA_USER_UID); pass empty to clear. Does NOT change the container OS user (always pravda, uid 1000).",
|
||||
"DevPlace user uid whose identity and API key are injected (DEVPLACE_API_KEY, DEVPLACE_USER_UID); pass empty to clear. Does NOT change the container OS user (always pravda, uid 1000).",
|
||||
),
|
||||
arg("boot_language", "Boot source language: 'none', 'python', or 'bash'."),
|
||||
arg("boot_script", "Boot source code body run on launch."),
|
||||
|
||||
@@ -287,7 +287,7 @@ class Dispatcher:
|
||||
self._rsearch = RsearchController(settings, owner_kind, owner_id)
|
||||
from ..container import ContainerController
|
||||
|
||||
self._container = ContainerController(client)
|
||||
self._container = ContainerController(client, owner_id=owner_id)
|
||||
from ..customization import CustomizationController
|
||||
|
||||
self._customization = CustomizationController(owner_kind, owner_id)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from devplacepy.database import get_table, resolve_by_slug
|
||||
from devplacepy.database import get_table, get_users_by_uids, resolve_by_slug
|
||||
from devplacepy.services.containers import api, store
|
||||
from devplacepy.services.containers.api import ContainerError
|
||||
from devplacepy.services.containers.runtime import get_backend
|
||||
@@ -15,8 +15,9 @@ logger = logging.getLogger("devii.container")
|
||||
|
||||
|
||||
class ContainerController:
|
||||
def __init__(self, client: Any = None) -> None:
|
||||
def __init__(self, client: Any = None, owner_id: str = "") -> None:
|
||||
self._client = client
|
||||
self._owner_id = owner_id
|
||||
|
||||
def _ingress_url(self, instance: dict):
|
||||
slug = instance.get("ingress_slug")
|
||||
@@ -29,11 +30,19 @@ class ContainerController:
|
||||
|
||||
def _actor_user(self) -> dict:
|
||||
username = getattr(self._client, "username", None)
|
||||
if username:
|
||||
if username and username != "api-key":
|
||||
user = get_table("users").find_one(username=username)
|
||||
if user:
|
||||
return user
|
||||
return {"uid": "admin", "username": username or "admin"}
|
||||
if self._owner_id:
|
||||
user = get_users_by_uids([self._owner_id]).get(self._owner_id)
|
||||
if user:
|
||||
return user
|
||||
return {
|
||||
"uid": "admin",
|
||||
"username": username or "admin",
|
||||
"role": "Admin",
|
||||
}
|
||||
|
||||
def _project(self, arguments: dict) -> dict:
|
||||
from devplacepy.content import can_view_project
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from devplacepy.cache import TTLCache
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
from .. import economy
|
||||
from .common import GameError, _farms, _iso, _now, _plots, _update_farm
|
||||
|
||||
|
||||
_leaderboard_cache = TTLCache(ttl=15, max_size=8)
|
||||
|
||||
|
||||
def get_farm(user_uid: str) -> dict | None:
|
||||
return _farms().find_one(user_uid=user_uid)
|
||||
|
||||
@@ -95,6 +99,9 @@ def upgrade_ci(user: dict) -> dict:
|
||||
|
||||
|
||||
def leaderboard(limit: int = 25) -> list[dict]:
|
||||
cached = _leaderboard_cache.get(f"top:{limit}")
|
||||
if cached is not None:
|
||||
return cached
|
||||
farms = sorted(
|
||||
_farms().find(),
|
||||
key=economy.farm_score,
|
||||
@@ -122,4 +129,5 @@ def leaderboard(limit: int = 25) -> list[dict]:
|
||||
"score": economy.farm_score(farm),
|
||||
}
|
||||
)
|
||||
_leaderboard_cache.set(f"top:{limit}", entries)
|
||||
return entries
|
||||
|
||||
Reference in New Issue
Block a user