Spaces:
Running
Running
Commit
·
08a6cc3
1
Parent(s):
ff3827e
Documentation
Browse files- README.md +7 -3
- api_client.py +4 -2
- api_monitor.py +16 -54
- app.py +1 -1
- keygenServer/client/app/main/page.tsx +2 -1
- killClaude.ps1 +0 -24
- schema.sql +0 -21
README.md
CHANGED
@@ -31,7 +31,7 @@ Hermes requires users to generate an MCP API key, which you can do right [here a
|
|
31 |
|
32 |
1. **Validate API Configuration**: Use the `validate_api_configuration` function to check if your API configuration is valid. If the request fails, it will return the error. Else, it will save the configuration to the database and return a success message with `config_id`. The user/agent can then decide whether to proceed with monitoring based on the response (etc. can reject the API configuration if it doesn't show the desired data content).
|
33 |
2. **Activate Monitoring**: Use the `activate_monitoring` function with the `config_id` from the previous step to start monitoring the API. This will set up a background task that will periodically check the API and store the responses.
|
34 |
-
3. **
|
35 |
|
36 |
### :warning: Important! :warning:
|
37 |
|
@@ -39,10 +39,14 @@ Call results and configurations will be deleted 14 days after the last call to t
|
|
39 |
|
40 |
## Problems we ran into
|
41 |
|
42 |
-
Since (free) Gradio is stateless, we had to find a way to pass down information between both the different API calls and the user calls, while still ensuring privacy. We ended up using the MCP API key as a user identifier, which allowed us to separate user data while still being able to access it when needed.
|
|
|
|
|
|
|
|
|
43 |
|
44 |
## Future Steps
|
45 |
|
46 |
-
Currently, the API call results store the entire response, which can be quite large, especially for LLMs like Claude to handle (or reach conversation limits). In the future, we think that we can improve this by allowing users to specify a format for the response, such as only storing the text content or a specific field in the response. This would allow users to save space and make it easier to analyze the data later on.
|
47 |
|
48 |
Also, we plan to move the authentication backend off of Render, as Render spins down the backend after a period of inactivity, which can cause a lot of delay. (We used Render because Vercel would not work with our database library.)
|
|
|
31 |
|
32 |
1. **Validate API Configuration**: Use the `validate_api_configuration` function to check if your API configuration is valid. If the request fails, it will return the error. Else, it will save the configuration to the database and return a success message with `config_id`. The user/agent can then decide whether to proceed with monitoring based on the response (etc. can reject the API configuration if it doesn't show the desired data content).
|
33 |
2. **Activate Monitoring**: Use the `activate_monitoring` function with the `config_id` from the previous step to start monitoring the API. This will set up a background task that will periodically check the API and store the responses.
|
34 |
+
3. **Retrieve Monitored Data**: Use the `retrieve_monitored_data` function with the `config_id` to retrieve the monitoring data. You can specify the mode of data retrieval (summary, details, or full) to get the desired level of information. This can be called anytime after monitoring is activated.
|
35 |
|
36 |
### :warning: Important! :warning:
|
37 |
|
|
|
39 |
|
40 |
## Problems we ran into
|
41 |
|
42 |
+
Since (free) Gradio is stateless, we had to find a way to pass down information between both the different API calls and the user calls, while still ensuring privacy. We ended up using the MCP API key as a user identifier, which allowed us to separate user data while still being able to access it when needed.
|
43 |
+
|
44 |
+
There was also the problem of having to validate the data and making sure that the user/agent is satisfied with the data before activating monitoring. We solved this by separating the whole process into multiple functions, which allowed us to validate the data before activating monitoring.
|
45 |
+
|
46 |
+
Another problem we ran into was finding a way to pass the MCP API key automatically to the API calls without having to manually input it every time. We were unavble to find a way to do this, as the server is remote and we couldn't upload an environment variable to the server.
|
47 |
|
48 |
## Future Steps
|
49 |
|
50 |
+
Currently, the API call results store the entire response, which can be quite large, especially for LLMs like Claude to handle (or reach conversation limits), hence the need for three different returns for monitored data. In the future, we think that we can improve this by allowing users to specify a format for the response, such as only storing the text content or a specific field in the response. This would allow users to save space and make it easier to analyze the data later on.
|
51 |
|
52 |
Also, we plan to move the authentication backend off of Render, as Render spins down the backend after a period of inactivity, which can cause a lot of delay. (We used Render because Vercel would not work with our database library.)
|
api_client.py
CHANGED
@@ -3,7 +3,7 @@ import json
|
|
3 |
|
4 |
|
5 |
def parse_key_value_string(key_value_string):
|
6 |
-
"""Parse a key-value string into a dictionary.
|
7 |
|
8 |
Parameters:
|
9 |
- key_value_string: String with key-value pairs, one per line, separated by ':'
|
@@ -89,7 +89,9 @@ def call_api(
|
|
89 |
x-api-key: your_api_key_here
|
90 |
content-type: application/json
|
91 |
additional_params: {"messages": [{"role": "user", "content": "Hello there!"}]}
|
92 |
-
"""
|
|
|
|
|
93 |
params = parse_key_value_string(param_keys_values)
|
94 |
headers = parse_key_value_string(header_keys_values)
|
95 |
|
|
|
3 |
|
4 |
|
5 |
def parse_key_value_string(key_value_string):
|
6 |
+
"""Parse a key-value string into a dictionary (meant for API arguments).
|
7 |
|
8 |
Parameters:
|
9 |
- key_value_string: String with key-value pairs, one per line, separated by ':'
|
|
|
89 |
x-api-key: your_api_key_here
|
90 |
content-type: application/json
|
91 |
additional_params: {"messages": [{"role": "user", "content": "Hello there!"}]}
|
92 |
+
"""
|
93 |
+
|
94 |
+
# Build params and headers dictionaries from key-value pairs
|
95 |
params = parse_key_value_string(param_keys_values)
|
96 |
headers = parse_key_value_string(header_keys_values)
|
97 |
|
api_monitor.py
CHANGED
@@ -200,6 +200,7 @@ def validate_api_configuration(
|
|
200 |
"config_id": None,
|
201 |
}
|
202 |
|
|
|
203 |
if not name or not name.strip():
|
204 |
return {
|
205 |
"success": False,
|
@@ -429,10 +430,8 @@ async def activate_monitoring(config_id, mcp_api_key):
|
|
429 |
this
|
430 |
"""
|
431 |
|
432 |
-
# using time_to_start, schedule_interval_minutes, and stop_after_hours
|
433 |
-
# label using name and description
|
434 |
|
435 |
-
#
|
436 |
try:
|
437 |
if not mcp_api_key or not mcp_api_key.strip() or mcp_api_key == "":
|
438 |
mcp_api_key = os.getenv("MCP_API_KEY", "")
|
@@ -442,6 +441,7 @@ async def activate_monitoring(config_id, mcp_api_key):
|
|
442 |
"message": "MCP API key is required",
|
443 |
"config_id": None,
|
444 |
}
|
|
|
445 |
# Verify the MCP API key with the key generation server first
|
446 |
key_verification = verify_mcp_api_key(mcp_api_key)
|
447 |
if not key_verification["success"]:
|
@@ -471,7 +471,9 @@ async def activate_monitoring(config_id, mcp_api_key):
|
|
471 |
"success": False,
|
472 |
"message": "Invalid mcp_api_key. You are not authorized to activate this configuration.",
|
473 |
"config_id": config_id,
|
474 |
-
}
|
|
|
|
|
475 |
name = config.get("name", "Unknown")
|
476 |
schedule_interval_minutes = float(config.get("schedule_interval_minutes", 20))
|
477 |
stop_at = config.get("stop_at")
|
@@ -487,7 +489,9 @@ async def activate_monitoring(config_id, mcp_api_key):
|
|
487 |
if not isinstance(stop_at, datetime):
|
488 |
stop_at = datetime.fromisoformat(
|
489 |
str(stop_at)
|
490 |
-
)
|
|
|
|
|
491 |
|
492 |
def api_monitoring_job():
|
493 |
now = datetime.now()
|
@@ -643,7 +647,9 @@ async def activate_monitoring(config_id, mcp_api_key):
|
|
643 |
except Exception as db_exc:
|
644 |
print(
|
645 |
f"Failed to log error to database: {db_exc}"
|
646 |
-
)
|
|
|
|
|
647 |
|
648 |
scheduler = AsyncIOScheduler()
|
649 |
# Schedule the API monitoring job
|
@@ -720,7 +726,7 @@ def retrieve_monitored_data(config_id, mcp_api_key, mode="summary"):
|
|
720 |
"timestamp": "2025-06-05T15:20:00",
|
721 |
"success": true,
|
722 |
"error": null,
|
723 |
-
"response_preview": "{'alerts': [{'type': 'tornado'}]}..." // truncated
|
724 |
}
|
725 |
// ... up to 5 most recent calls
|
726 |
],
|
@@ -935,7 +941,7 @@ def retrieve_monitored_data(config_id, mcp_api_key, mode="summary"):
|
|
935 |
"success": item.get("is_successful", False),
|
936 |
"response_data": item.get(
|
937 |
"response_data"
|
938 |
-
),
|
939 |
"error": (
|
940 |
item.get("error_message")
|
941 |
if not item.get("is_successful")
|
@@ -967,7 +973,7 @@ def retrieve_monitored_data(config_id, mcp_api_key, mode="summary"):
|
|
967 |
else None
|
968 |
),
|
969 |
"response_preview": (
|
970 |
-
str(item.get("response_data", ""))[:
|
971 |
if item.get("response_data")
|
972 |
else None
|
973 |
),
|
@@ -990,48 +996,4 @@ def retrieve_monitored_data(config_id, mcp_api_key, mode="summary"):
|
|
990 |
"success": False,
|
991 |
"message": f"Database connection failed: {str(e)}",
|
992 |
"data": [],
|
993 |
-
}
|
994 |
-
|
995 |
-
|
996 |
-
## testing
|
997 |
-
import asyncio
|
998 |
-
|
999 |
-
|
1000 |
-
async def main():
|
1001 |
-
|
1002 |
-
validation_response = validate_api_configuration(
|
1003 |
-
mcp_api_key=os.getenv("MCP_API_KEY"),
|
1004 |
-
name="Dog Facts API",
|
1005 |
-
description="Monitor random dog facts from a free API",
|
1006 |
-
method="GET",
|
1007 |
-
base_url="https://dogapi.dog",
|
1008 |
-
endpoint="api/v2/facts",
|
1009 |
-
param_keys_values="",
|
1010 |
-
header_keys_values="",
|
1011 |
-
additional_params="{}",
|
1012 |
-
schedule_interval_minutes=0.05,
|
1013 |
-
stop_after_hours=1,
|
1014 |
-
start_at="",
|
1015 |
-
)
|
1016 |
-
print(validation_response)
|
1017 |
-
print()
|
1018 |
-
print()
|
1019 |
-
|
1020 |
-
activate_monitoring_response = await activate_monitoring(
|
1021 |
-
config_id=validation_response.get("config_id"),
|
1022 |
-
mcp_api_key="os.getenv('MCP_API_KEY')",
|
1023 |
-
)
|
1024 |
-
print(activate_monitoring_response)
|
1025 |
-
print()
|
1026 |
-
print()
|
1027 |
-
|
1028 |
-
await asyncio.sleep(10)
|
1029 |
-
response = retrieve_monitored_data(
|
1030 |
-
config_id=activate_monitoring_response.get("config_id"),
|
1031 |
-
mcp_api_key=os.getenv("MCP_API_KEY"),
|
1032 |
-
)
|
1033 |
-
print(json.dumps(response, indent=2, default=str))
|
1034 |
-
|
1035 |
-
|
1036 |
-
if __name__ == "__main__":
|
1037 |
-
asyncio.run(main())
|
|
|
200 |
"config_id": None,
|
201 |
}
|
202 |
|
203 |
+
# Validate required parameters
|
204 |
if not name or not name.strip():
|
205 |
return {
|
206 |
"success": False,
|
|
|
430 |
this
|
431 |
"""
|
432 |
|
|
|
|
|
433 |
|
434 |
+
# Attempt to create the scheduler
|
435 |
try:
|
436 |
if not mcp_api_key or not mcp_api_key.strip() or mcp_api_key == "":
|
437 |
mcp_api_key = os.getenv("MCP_API_KEY", "")
|
|
|
441 |
"message": "MCP API key is required",
|
442 |
"config_id": None,
|
443 |
}
|
444 |
+
|
445 |
# Verify the MCP API key with the key generation server first
|
446 |
key_verification = verify_mcp_api_key(mcp_api_key)
|
447 |
if not key_verification["success"]:
|
|
|
471 |
"success": False,
|
472 |
"message": "Invalid mcp_api_key. You are not authorized to activate this configuration.",
|
473 |
"config_id": config_id,
|
474 |
+
}
|
475 |
+
|
476 |
+
# Extract scheduling parameters
|
477 |
name = config.get("name", "Unknown")
|
478 |
schedule_interval_minutes = float(config.get("schedule_interval_minutes", 20))
|
479 |
stop_at = config.get("stop_at")
|
|
|
489 |
if not isinstance(stop_at, datetime):
|
490 |
stop_at = datetime.fromisoformat(
|
491 |
str(stop_at)
|
492 |
+
)
|
493 |
+
|
494 |
+
# Job function to make actual API calls
|
495 |
|
496 |
def api_monitoring_job():
|
497 |
now = datetime.now()
|
|
|
647 |
except Exception as db_exc:
|
648 |
print(
|
649 |
f"Failed to log error to database: {db_exc}"
|
650 |
+
)
|
651 |
+
|
652 |
+
# Setup AsyncIO scheduler
|
653 |
|
654 |
scheduler = AsyncIOScheduler()
|
655 |
# Schedule the API monitoring job
|
|
|
726 |
"timestamp": "2025-06-05T15:20:00",
|
727 |
"success": true,
|
728 |
"error": null,
|
729 |
+
"response_preview": "{'alerts': [{'type': 'tornado'}]}..." // truncated to 150 characters
|
730 |
}
|
731 |
// ... up to 5 most recent calls
|
732 |
],
|
|
|
941 |
"success": item.get("is_successful", False),
|
942 |
"response_data": item.get(
|
943 |
"response_data"
|
944 |
+
),
|
945 |
"error": (
|
946 |
item.get("error_message")
|
947 |
if not item.get("is_successful")
|
|
|
973 |
else None
|
974 |
),
|
975 |
"response_preview": (
|
976 |
+
str(item.get("response_data", ""))[:150] + "..."
|
977 |
if item.get("response_data")
|
978 |
else None
|
979 |
),
|
|
|
996 |
"success": False,
|
997 |
"message": f"Database connection failed: {str(e)}",
|
998 |
"data": [],
|
999 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -165,7 +165,7 @@ retrieve_tab = gr.Interface(
|
|
165 |
demo = gr.TabbedInterface(
|
166 |
[validation_tab, scheduler_tab, retrieve_tab],
|
167 |
["Validate & Store", "Activate Scheduler", "Retrieve Data"],
|
168 |
-
title="
|
169 |
)
|
170 |
|
171 |
if __name__ == "__main__":
|
|
|
165 |
demo = gr.TabbedInterface(
|
166 |
[validation_tab, scheduler_tab, retrieve_tab],
|
167 |
["Validate & Store", "Activate Scheduler", "Retrieve Data"],
|
168 |
+
title="Hermes - Automated Asynchronous REST API Monitoring",
|
169 |
)
|
170 |
|
171 |
if __name__ == "__main__":
|
keygenServer/client/app/main/page.tsx
CHANGED
@@ -114,7 +114,8 @@ export default function MainPage() {
|
|
114 |
</div>
|
115 |
|
116 |
{/* Main Content */}
|
117 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
|
|
118 |
<div className="neuro-card p-4 md:p-6">
|
119 |
<h2 className="text-lg md:text-xl font-semibold text-primary mb-4">
|
120 |
Generate MCP API Key
|
|
|
114 |
</div>
|
115 |
|
116 |
{/* Main Content */}
|
117 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
118 |
+
{/* API Key Generation */}
|
119 |
<div className="neuro-card p-4 md:p-6">
|
120 |
<h2 className="text-lg md:text-xl font-semibold text-primary mb-4">
|
121 |
Generate MCP API Key
|
killClaude.ps1
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
$claudePath = "C:\Users\colem\AppData\Local\AnthropicClaude\claude.exe"
|
2 |
-
|
3 |
-
# Get all processes named "claude"
|
4 |
-
$processes = Get-Process -Name "claude" -ErrorAction SilentlyContinue
|
5 |
-
|
6 |
-
foreach ($proc in $processes) {
|
7 |
-
try {
|
8 |
-
Write-Output "Killing process: $($proc.Name) (ID: $($proc.Id))"
|
9 |
-
Stop-Process -Id $proc.Id -Force -ErrorAction Stop
|
10 |
-
Start-Sleep -Milliseconds 300
|
11 |
-
}
|
12 |
-
catch {
|
13 |
-
Write-Output "Process with ID $($proc.Id) already exited or could not be terminated."
|
14 |
-
}
|
15 |
-
}
|
16 |
-
|
17 |
-
# Start the application again
|
18 |
-
if (Test-Path $claudePath) {
|
19 |
-
Write-Output "Restarting Claude..."
|
20 |
-
Start-Process $claudePath
|
21 |
-
}
|
22 |
-
else {
|
23 |
-
Write-Output "Error: Claude executable not found at $claudePath"
|
24 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
schema.sql
CHANGED
@@ -49,24 +49,3 @@ INSERT INTO api_configurations (
|
|
49 |
'2025-06-04T12:00:00',
|
50 |
'2025-06-11T12:00:00'
|
51 |
);
|
52 |
-
|
53 |
-
INSERT INTO api_call_results (
|
54 |
-
config_id, response_data, is_successful, error_message
|
55 |
-
) VALUES (
|
56 |
-
10101,
|
57 |
-
'{"symbol":"NVDA", "price":1142.50, "timestamp":"2025-06-03T14:20:00Z"}',
|
58 |
-
TRUE,
|
59 |
-
NULL
|
60 |
-
);
|
61 |
-
|
62 |
-
INSERT INTO api_call_results (
|
63 |
-
config_id, response_data, is_successful, error_message
|
64 |
-
) VALUES (
|
65 |
-
10101,
|
66 |
-
NULL,
|
67 |
-
FALSE,
|
68 |
-
'Timeout while contacting the API'
|
69 |
-
);
|
70 |
-
|
71 |
-
select * from api_configurations;
|
72 |
-
select * from api_call_results;
|
|
|
49 |
'2025-06-04T12:00:00',
|
50 |
'2025-06-11T12:00:00'
|
51 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|