Compare commits
12 Commits
38a5a29bb6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 829539bdb8 | |||
| 80510370b0 | |||
| 102c146b60 | |||
| 6ee9d2429c | |||
| 6532bd250d | |||
| 54840def97 | |||
| 5d679421de | |||
|
|
eb42400e5b | ||
|
|
73335050aa | ||
|
|
cd79af6599 | ||
|
|
972979a44d | ||
|
|
990be58fc0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.shsf.json
|
.shsf.json
|
||||||
prompt.txt
|
prompt.txt
|
||||||
|
__pycache__/
|
||||||
|
|||||||
@@ -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_URL` | The endpoint URL for the OpenClaw API. (with hook path) |
|
||||||
| `OPENCLAW_TOKEN` | The authentication token for OpenClaw. (from hooks, not gateway ui) |
|
| `OPENCLAW_TOKEN` | The authentication token for OpenClaw. (from hooks, not gateway ui) |
|
||||||
| `OPENCLAW_PROXY_AUTH` | (Optional) Proxy credentials in `username:password` format. |
|
| `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. |
|
| `DATABASE_STORAGE_NAME` | The name of the database storage to use. |
|
||||||
| `AGENT_USERNAME` | The Gitea username of the AI agent (e.g., `whateveryouragentisnamed`). |
|
| `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_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
|
## How it works
|
||||||
|
|
||||||
|
|||||||
108
main.py
108
main.py
@@ -7,9 +7,11 @@ OPENCLAW_URL = os.getenv("OPENCLAW_URL")
|
|||||||
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN")
|
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN")
|
||||||
OPENCLAW_PROXY_AUTH = os.getenv("OPENCLAW_PROXY_AUTH")
|
OPENCLAW_PROXY_AUTH = os.getenv("OPENCLAW_PROXY_AUTH")
|
||||||
OPENCLAW_MODEL = os.getenv("OPENCLAW_MODEL")
|
OPENCLAW_MODEL = os.getenv("OPENCLAW_MODEL")
|
||||||
|
OPENCLAW_THINKING = os.getenv("OPENCLAW_THINKING", "low")
|
||||||
DATABASE_STORAGE = os.getenv("DATABASE_STORAGE_NAME")
|
DATABASE_STORAGE = os.getenv("DATABASE_STORAGE_NAME")
|
||||||
AGENT_USERNAME = os.getenv("AGENT_USERNAME")
|
AGENT_USERNAME = os.getenv("AGENT_USERNAME")
|
||||||
AGENT_PROMPT_FILE = os.getenv("AGENT_PROMPT_FILE")
|
AGENT_PROMPT_FILE = os.getenv("AGENT_PROMPT_FILE")
|
||||||
|
AGENT_HOURLY = int(os.getenv("AGENT_HOURLY", "60"))
|
||||||
|
|
||||||
eventsToHandle = ["pull_request", "issues", "issue_comment"]
|
eventsToHandle = ["pull_request", "issues", "issue_comment"]
|
||||||
actionsToHandle = ["assigned", "created"]
|
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}")
|
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):
|
def fill_template(template, event_object, action_str, type_str):
|
||||||
fields = {
|
fields = {
|
||||||
"action_str": action_str,
|
"action_str": action_str,
|
||||||
@@ -102,24 +141,35 @@ def build_message(event_object, action_str, type_str):
|
|||||||
print(
|
print(
|
||||||
f"Custom prompt file not found at {AGENT_PROMPT_FILE_PATH}. Using default message."
|
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 = fill_template(
|
||||||
message = message.replace("You were [action_str] to an [type_str]", action_summary)
|
DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str
|
||||||
|
)
|
||||||
|
message = message.replace(
|
||||||
|
"You were [action_str] to an [type_str]", action_summary
|
||||||
|
)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
with open(AGENT_PROMPT_FILE_PATH, "r") as f:
|
with open(AGENT_PROMPT_FILE_PATH, "r") as f:
|
||||||
custom_prompt = f.read()
|
custom_prompt = f.read()
|
||||||
message = fill_template(custom_prompt, event_object, action_str, type_str)
|
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
|
return message
|
||||||
else:
|
else:
|
||||||
print("No custom prompt file specified. Using default message.")
|
print("No custom prompt file specified. Using default message.")
|
||||||
message = fill_template(DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str)
|
message = fill_template(
|
||||||
message = message.replace("You were [action_str] to an [type_str]", action_summary)
|
DEFAULT_PROMPT_TEMPLATE, event_object, action_str, type_str
|
||||||
|
)
|
||||||
|
message = message.replace(
|
||||||
|
"You were [action_str] to an [type_str]", action_summary
|
||||||
|
)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
def sendToAgent(event_object):
|
def sendToAgent(event_object, db):
|
||||||
headers = {"x-openclaw-token": OPENCLAW_TOKEN, "Content-Type": "application/json"}
|
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":
|
if event_object.get("type") == "issue_comment":
|
||||||
action_str = "created"
|
action_str = "created"
|
||||||
@@ -134,13 +184,20 @@ def sendToAgent(event_object):
|
|||||||
else:
|
else:
|
||||||
action_str = "assigned"
|
action_str = "assigned"
|
||||||
type_str = "issue" if event_object["type"] == "issue" else "pull request"
|
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:
|
if AGENT_USERNAME not in assignees:
|
||||||
print(
|
print(
|
||||||
f"Agent {AGENT_USERNAME} is not among the assignees for this event. Skipping notification."
|
f"Agent {AGENT_USERNAME} is not among the assignees for this event. Skipping notification."
|
||||||
)
|
)
|
||||||
return
|
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)
|
message = build_message(event_object, action_str, type_str)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -153,9 +210,9 @@ def sendToAgent(event_object):
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
"message": message,
|
"message": message,
|
||||||
"thinking": "low",
|
"thinking": OPENCLAW_THINKING,
|
||||||
"timeoutSeconds": 45,
|
|
||||||
"model": OPENCLAW_MODEL,
|
"model": OPENCLAW_MODEL,
|
||||||
|
"deliver": False,
|
||||||
},
|
},
|
||||||
auth=auth,
|
auth=auth,
|
||||||
)
|
)
|
||||||
@@ -339,15 +396,9 @@ def main(args):
|
|||||||
db, event_type, repo_id, event_object["number"], assignees_list
|
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
|
# Send to OpenClaw
|
||||||
if action == "assigned":
|
if action == "assigned":
|
||||||
sendToAgent(event_object)
|
sendToAgent(event_object, db)
|
||||||
else:
|
else:
|
||||||
print(f"Action {action} is not configured to send to agent")
|
print(f"Action {action} is not configured to send to agent")
|
||||||
|
|
||||||
@@ -373,12 +424,7 @@ def main(args):
|
|||||||
"url": comment_data.get("html_url"),
|
"url": comment_data.get("html_url"),
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"--- {event_object['type'].upper()} {action.capitalize()} ---")
|
sendToAgent(event_object, db)
|
||||||
print(f"Title: {event_object['title']}")
|
|
||||||
print(f"Repo: {event_object['repository']}")
|
|
||||||
print(f"By: {event_object['sender']}")
|
|
||||||
|
|
||||||
sendToAgent(event_object)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"_shsf": "v2",
|
"_shsf": "v2",
|
||||||
@@ -386,6 +432,24 @@ def main(args):
|
|||||||
"_res": {"status": "received"},
|
"_res": {"status": "received"},
|
||||||
"_headers": {"Content-Type": "application/json"},
|
"_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:
|
else:
|
||||||
return {
|
return {
|
||||||
"_shsf": "v2",
|
"_shsf": "v2",
|
||||||
|
|||||||
Reference in New Issue
Block a user