Compare commits

..

12 Commits

Author SHA1 Message Date
829539bdb8 Merge pull request 'feat: hourly rate limits for triggers' (#8) from feat/hourly-ratelimits into main
All checks were successful
Lint and Syntax Check / build (push) Successful in 5s
Reviewed-on: #8
2026-04-04 17:25:03 +02:00
80510370b0 docs: add AGENT_HOURLY env variable to README
All checks were successful
Lint and Syntax Check / build (pull_request) Successful in 12s
2026-04-04 17:23:00 +02:00
102c146b60 Delete __pycache__/main.cpython-313.pyc
All checks were successful
Lint and Syntax Check / build (pull_request) Successful in 12s
2026-04-04 17:20:01 +02:00
6ee9d2429c Add __pycache__/ to .gitignore and remove cached pyc file
All checks were successful
Lint and Syntax Check / build (pull_request) Successful in 5s
2026-04-04 17:19:16 +02:00
6532bd250d feat: add hourly rate limits for agent triggers
All checks were successful
Lint and Syntax Check / build (pull_request) Successful in 6s
- Add AGENT_HOURLY env var (default: 60 triggers per hour)
- Track hourly usage in SHSF database with key 'agent_{AGENT_USERNAME}_cooldown'
- Check and increment counter before sending notifications
- Add 'clear_limit' route to reset counter hourly via external trigger

Resolves #4
2026-04-04 17:10:23 +02:00
54840def97 Merge pull request 'feat: Custom Thinking effort' (#7) from feat/custom-thinking-effort into main
All checks were successful
Lint and Syntax Check / build (push) Successful in 12s
Reviewed-on: #7
2026-04-04 16:57:48 +02:00
5d679421de feat: add support for custom thinking effort (resolves #6)
All checks were successful
Lint and Syntax Check / build (pull_request) Successful in 1m45s
2026-04-04 16:27:45 +02:00
Space-Banane
eb42400e5b removed timeout
Some checks failed
Lint and Syntax Check / build (push) Failing after 11s
2026-04-04 16:24:36 +02:00
Space-Banane
73335050aa fix: improve message handling and formatting in build_message function
All checks were successful
Lint and Syntax Check / build (push) Successful in 11s
2026-04-04 16:17:58 +02:00
Space-Banane
cd79af6599 Dump json object
All checks were successful
Lint and Syntax Check / build (push) Successful in 11s
2026-04-04 15:21:10 +02:00
Space-Banane
972979a44d removed sample prompts
All checks were successful
Lint and Syntax Check / build (push) Successful in 11s
2026-04-04 14:57:04 +02:00
Space-Banane
990be58fc0 feat: add dispatched Gitea action prompt template
All checks were successful
Lint and Syntax Check / build (push) Successful in 6s
2026-04-04 14:26:27 +02:00
3 changed files with 90 additions and 23 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.shsf.json
prompt.txt
__pycache__/

View File

@@ -19,9 +19,11 @@ The following environment variables are required for the service to function:
| `OPENCLAW_URL` | The endpoint URL for the OpenClaw API. (with hook path) |
| `OPENCLAW_TOKEN` | The authentication token for OpenClaw. (from hooks, not gateway ui) |
| `OPENCLAW_PROXY_AUTH` | (Optional) Proxy credentials in `username:password` format. |
| `OPENCLAW_THINKING` | (Optional) The thinking effort for the agent (e.g., `low`, `medium`, `high`). Defaults to `low`. |
| `DATABASE_STORAGE_NAME` | The name of the database storage to use. |
| `AGENT_USERNAME` | The Gitea username of the AI agent (e.g., `whateveryouragentisnamed`). |
| `AGENT_PROMPT_FILE` | (Optional) Path to a custom Markdown template for notifications. If not provided, a default template will be used. |
| `AGENT_HOURLY` | (Optional) Maximum number of triggers allowed per hour. Defaults to `60`. Used for rate limiting. |
## How it works

108
main.py
View File

@@ -7,9 +7,11 @@ OPENCLAW_URL = os.getenv("OPENCLAW_URL")
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN")
OPENCLAW_PROXY_AUTH = os.getenv("OPENCLAW_PROXY_AUTH")
OPENCLAW_MODEL = os.getenv("OPENCLAW_MODEL")
OPENCLAW_THINKING = os.getenv("OPENCLAW_THINKING", "low")
DATABASE_STORAGE = os.getenv("DATABASE_STORAGE_NAME")
AGENT_USERNAME = os.getenv("AGENT_USERNAME")
AGENT_PROMPT_FILE = os.getenv("AGENT_PROMPT_FILE")
AGENT_HOURLY = int(os.getenv("AGENT_HOURLY", "60"))
eventsToHandle = ["pull_request", "issues", "issue_comment"]
actionsToHandle = ["assigned", "created"]
@@ -75,6 +77,43 @@ def delete_stored_assignees(db, event_type, repository_id, number):
print(f"Failed to delete assignees from DB: {e}")
def get_rate_limit_key():
return f"agent_{AGENT_USERNAME}_cooldown"
def check_and_increment_rate_limit(db):
"""Check if rate limit is reached, increment counter if not. Returns True if allowed, False if limit reached."""
key = get_rate_limit_key()
try:
current = db.get(DATABASE_STORAGE, key)
if current is None:
current = 0
else:
current = int(current)
if current >= AGENT_HOURLY:
print(f"Rate limit reached: {current}/{AGENT_HOURLY} hourly triggers used.")
return False
db.set(DATABASE_STORAGE, key, str(current + 1))
print(f"Rate limit check passed: {current + 1}/{AGENT_HOURLY} hourly triggers used.")
return True
except Exception as e:
print(f"Failed to check/increment rate limit: {e}")
# On error, allow the request to proceed
return True
def reset_rate_limit(db):
"""Reset the hourly rate limit counter."""
key = get_rate_limit_key()
try:
db.set(DATABASE_STORAGE, key, "0")
print(f"Rate limit reset for agent {AGENT_USERNAME}.")
except Exception as e:
print(f"Failed to reset rate limit: {e}")
def fill_template(template, event_object, action_str, type_str):
fields = {
"action_str": action_str,
@@ -102,24 +141,35 @@ def build_message(event_object, action_str, type_str):
print(
f"Custom prompt file not found at {AGENT_PROMPT_FILE_PATH}. Using default message."
)
message = fill_template(DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str)
message = message.replace("You were [action_str] to an [type_str]", action_summary)
message = fill_template(
DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str
)
message = message.replace(
"You were [action_str] to an [type_str]", action_summary
)
return message
with open(AGENT_PROMPT_FILE_PATH, "r") as f:
custom_prompt = f.read()
message = fill_template(custom_prompt, event_object, action_str, type_str)
message = message.replace("You were [action_str] to an [type_str]", action_summary)
message = message.replace(
"You were [action_str] to an [type_str]", action_summary
)
return message
else:
print("No custom prompt file specified. Using default message.")
message = fill_template(DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str)
message = message.replace("You were [action_str] to an [type_str]", action_summary)
message = fill_template(
DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str
)
message = message.replace(
"You were [action_str] to an [type_str]", action_summary
)
return message
def sendToAgent(event_object):
def sendToAgent(event_object, db):
headers = {"x-openclaw-token": OPENCLAW_TOKEN, "Content-Type": "application/json"}
print(f"Preparing to send notification to Agent for {json.dumps(event_object)}")
if event_object.get("type") == "issue_comment":
action_str = "created"
@@ -134,13 +184,20 @@ def sendToAgent(event_object):
else:
action_str = "assigned"
type_str = "issue" if event_object["type"] == "issue" else "pull request"
assignees = event_object.get("assignees", [event_object.get("assignee", "Unknown")])
assignees = event_object.get(
"assignees", [event_object.get("assignee", "Unknown")]
)
if AGENT_USERNAME not in assignees:
print(
f"Agent {AGENT_USERNAME} is not among the assignees for this event. Skipping notification."
)
return
# Check rate limit before sending
if not check_and_increment_rate_limit(db):
print("Rate limit reached. Skipping notification to agent.")
return
message = build_message(event_object, action_str, type_str)
try:
@@ -153,9 +210,9 @@ def sendToAgent(event_object):
headers=headers,
json={
"message": message,
"thinking": "low",
"timeoutSeconds": 45,
"thinking": OPENCLAW_THINKING,
"model": OPENCLAW_MODEL,
"deliver": False,
},
auth=auth,
)
@@ -339,15 +396,9 @@ def main(args):
db, event_type, repo_id, event_object["number"], assignees_list
)
print(f"--- {event_object['type'].upper()} {action.capitalize()} ---")
print(f"Title: {event_object['title']}")
print(f"Assignees: {', '.join(event_object['assignees'])}")
print(f"Repo: {event_object['repository']}")
print(f"By: {event_object['sender']}")
# Send to OpenClaw
if action == "assigned":
sendToAgent(event_object)
sendToAgent(event_object, db)
else:
print(f"Action {action} is not configured to send to agent")
@@ -373,12 +424,7 @@ def main(args):
"url": comment_data.get("html_url"),
}
print(f"--- {event_object['type'].upper()} {action.capitalize()} ---")
print(f"Title: {event_object['title']}")
print(f"Repo: {event_object['repository']}")
print(f"By: {event_object['sender']}")
sendToAgent(event_object)
sendToAgent(event_object, db)
return {
"_shsf": "v2",
@@ -386,6 +432,24 @@ def main(args):
"_res": {"status": "received"},
"_headers": {"Content-Type": "application/json"},
}
elif route == "clear_limit":
# Reset the hourly rate limit counter
try:
reset_rate_limit(db)
return {
"_shsf": "v2",
"_code": 200,
"_res": {"status": "Rate limit reset"},
"_headers": {"Content-Type": "application/json"},
}
except Exception as e:
print(f"Failed to reset rate limit: {e}")
return {
"_shsf": "v2",
"_code": 500,
"_res": {"error": "Failed to reset rate limit"},
"_headers": {"Content-Type": "application/json"},
}
else:
return {
"_shsf": "v2",