fixxxeeess
This commit is contained in:
parent
643203b8a5
commit
c4d9ca19ce
8 changed files with 142 additions and 54 deletions
|
|
@ -16,7 +16,8 @@ const state = {
|
|||
selectedRunId: null,
|
||||
currentView: 'dashboard', // 'dashboard' or 'task'
|
||||
isEditing: false,
|
||||
token: localStorage.getItem('auth_token')
|
||||
token: localStorage.getItem('auth_token'),
|
||||
refreshToken: localStorage.getItem('refresh_token')
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
|
|
@ -67,7 +68,24 @@ async function fetchWithAuth(url, options = {}) {
|
|||
'Authorization': `Bearer ${state.token}`
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
let response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401 && state.refreshToken) {
|
||||
// Try to refresh token
|
||||
try {
|
||||
const success = await attemptTokenRefresh();
|
||||
if (success) {
|
||||
// Retry original request with new token
|
||||
const newHeaders = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${state.token}`
|
||||
};
|
||||
response = await fetch(url, { ...options, headers: newHeaders });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
|
|
@ -77,6 +95,32 @@ async function fetchWithAuth(url, options = {}) {
|
|||
return response;
|
||||
}
|
||||
|
||||
async function attemptTokenRefresh() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: state.refreshToken })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.access_token) {
|
||||
state.token = data.access_token;
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
if (data.refresh_token) {
|
||||
state.refreshToken = data.refresh_token;
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during token refresh:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/tasks`);
|
||||
|
|
@ -134,10 +178,10 @@ async function fetchRecentRuns() {
|
|||
function renderTaskList() {
|
||||
const sortedTasks = [...state.tasks].sort((a, b) => {
|
||||
const aDate = a.runs && a.runs.length > 0
|
||||
? new Date(a.runs[a.runs.length - 1].created_at)
|
||||
? new Date(a.runs[0].created_at)
|
||||
: new Date(a.created_at);
|
||||
const bDate = b.runs && b.runs.length > 0
|
||||
? new Date(b.runs[b.runs.length - 1].created_at)
|
||||
? new Date(b.runs[0].created_at)
|
||||
: new Date(b.created_at);
|
||||
return bDate - aDate;
|
||||
});
|
||||
|
|
@ -145,7 +189,7 @@ function renderTaskList() {
|
|||
taskListEl.innerHTML = sortedTasks
|
||||
.map((task) => {
|
||||
const latestRun = task.runs && task.runs.length > 0
|
||||
? task.runs[task.runs.length - 1]
|
||||
? task.runs[0]
|
||||
: null;
|
||||
const status = latestRun ? latestRun.status : 'pending';
|
||||
const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at);
|
||||
|
|
@ -192,8 +236,6 @@ function selectTask(id, runId = null) {
|
|||
|
||||
function renderRunHistory(task) {
|
||||
runListEl.innerHTML = task.runs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map(
|
||||
(run, index) => `
|
||||
<li class="run-item ${state.selectedRunId === run.id ? 'active' : ''}" data-id="${run.id}">
|
||||
|
|
@ -250,7 +292,7 @@ function showTaskView(task) {
|
|||
dashboardViewEl.classList.add('hidden');
|
||||
taskViewEl.classList.remove('hidden');
|
||||
|
||||
const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[task.runs.length - 1];
|
||||
const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[0];
|
||||
|
||||
viewGoalEl.textContent = task.goal;
|
||||
|
||||
|
|
@ -473,7 +515,9 @@ async function showLogin() {
|
|||
|
||||
async function logout() {
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
showLogin();
|
||||
}
|
||||
|
||||
|
|
@ -493,6 +537,10 @@ async function handleCallback() {
|
|||
if (data.access_token) {
|
||||
state.token = data.access_token;
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
if (data.refresh_token) {
|
||||
state.refreshToken = data.refresh_token;
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
callbackOverlay.classList.add('hidden');
|
||||
appEl.classList.remove('hidden');
|
||||
initializeApp();
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ body {
|
|||
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -246,7 +246,7 @@ body {
|
|||
display: flex;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.run-history {
|
||||
|
|
@ -272,6 +272,7 @@ body {
|
|||
|
||||
#run-list {
|
||||
list-style: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
|
|
|||
14
src/agent.rs
14
src/agent.rs
|
|
@ -19,13 +19,13 @@ impl Agent {
|
|||
zen_api_key: Option<String>,
|
||||
tavily_api_key: Option<String>,
|
||||
initial_message: String,
|
||||
) -> Self {
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let intro = format!(
|
||||
"You are an autonomous agent. You have access to tools that can help
|
||||
you achieve your goals. Use them wisely. The user is unable to respond to you
|
||||
so do not ask for clarification and use the
|
||||
answer tool once you to give your final answer. current date is {}",
|
||||
Utc::now().to_rfc3339()
|
||||
Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string()
|
||||
);
|
||||
println!("initial_message: {}", intro);
|
||||
let messages = vec![
|
||||
|
|
@ -45,8 +45,12 @@ impl Agent {
|
|||
|
||||
let tools = Some(tools::get_tools());
|
||||
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
|
||||
zen_api_key,
|
||||
tavily_api_key,
|
||||
|
|
@ -54,7 +58,7 @@ impl Agent {
|
|||
tools,
|
||||
logs: String::new(),
|
||||
answer: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn log(&mut self, message: &str) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ pub async fn perform_search(
|
|||
query: &str,
|
||||
api_key: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
let response = client
|
||||
.post("https://api.tavily.com/search")
|
||||
.json(&serde_json::json!({
|
||||
|
|
|
|||
29
src/auth.rs
29
src/auth.rs
|
|
@ -15,11 +15,13 @@ pub struct Claims {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Jwk {
|
||||
kty: String,
|
||||
#[serde(rename = "kty")]
|
||||
_kty: String,
|
||||
kid: String,
|
||||
n: String,
|
||||
e: String,
|
||||
alg: Option<String>,
|
||||
#[serde(rename = "alg")]
|
||||
_alg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -146,4 +148,27 @@ impl Authenticator {
|
|||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn refresh_token(
|
||||
&self,
|
||||
refresh_token: String,
|
||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let params = [
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", &refresh_token),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
];
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(&self.token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ impl Scheduler {
|
|||
run.insert(&db).await?;
|
||||
|
||||
// Start agent in background
|
||||
let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone());
|
||||
let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone())?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (logs, answer) = match agent.run().await {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ pub async fn start(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||
.route("/tasks/:id/runs", post(rerun_task))
|
||||
.route("/runs/recent", get(get_recent_runs))
|
||||
.route("/auth/callback", get(auth_callback))
|
||||
.route("/auth/refresh", post(auth_refresh))
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
|
|
@ -150,7 +151,9 @@ async fn list_tasks(
|
|||
|
||||
let response = tasks
|
||||
.into_iter()
|
||||
.map(|(t, runs)| TaskResponse {
|
||||
.map(|(t, mut runs)| {
|
||||
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
TaskResponse {
|
||||
id: t.id,
|
||||
goal: t.goal,
|
||||
cron: t.cron,
|
||||
|
|
@ -165,6 +168,7 @@ async fn list_tasks(
|
|||
created_at: r.created_at,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -240,7 +244,8 @@ async fn execute_agent_run(
|
|||
state.zen_api_key.clone(),
|
||||
state.tavily_api_key.clone(),
|
||||
goal.clone(),
|
||||
);
|
||||
)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let (logs, answer) = match agent.run().await {
|
||||
Ok((logs, answer)) => (logs, answer),
|
||||
|
|
@ -298,6 +303,7 @@ async fn update_task(
|
|||
get_task_inner(id, &state).await.map(Json)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct AuthenticatedUser(pub crate::auth::Claims);
|
||||
|
||||
#[axum::async_trait]
|
||||
|
|
@ -342,6 +348,23 @@ pub struct AuthCallbackQuery {
|
|||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RefreshRequest {
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
async fn auth_refresh(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<RefreshRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
state
|
||||
.authenticator
|
||||
.refresh_token(payload.refresh_token)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))
|
||||
}
|
||||
|
||||
async fn auth_callback(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<AuthCallbackQuery>,
|
||||
|
|
@ -374,11 +397,13 @@ async fn get_task_inner(id: Uuid, state: &AppState) -> Result<TaskResponse, (Sta
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let (t, runs) = results
|
||||
let (t, mut runs) = results
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or((StatusCode::NOT_FOUND, "Task not found".to_string()))?;
|
||||
|
||||
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
Ok(TaskResponse {
|
||||
id: t.id,
|
||||
goal: t.goal,
|
||||
|
|
|
|||
19
src/tools.rs
19
src/tools.rs
|
|
@ -44,7 +44,6 @@ pub async fn handle_tool_call(
|
|||
tool_call: &ToolCall,
|
||||
tavily_api_key: &Option<String>,
|
||||
) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> {
|
||||
let mut file_written = false;
|
||||
let mut answer = None;
|
||||
let name = &tool_call.function.name;
|
||||
|
||||
|
|
@ -66,30 +65,14 @@ pub async fn handle_tool_call(
|
|||
"Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string()
|
||||
};
|
||||
(search_result, false)
|
||||
} else if name == "write_file" {
|
||||
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
|
||||
let path = args.get("path").ok_or("Missing path argument")?;
|
||||
let content = args.get("content").ok_or("Missing content argument")?;
|
||||
|
||||
println!("--- Executing tool: write_file(path: \"{}\") ---", path);
|
||||
|
||||
let write_result = match std::fs::write(path, content) {
|
||||
Ok(_) => {
|
||||
file_written = true;
|
||||
format!("Successfully wrote content to {}", path)
|
||||
}
|
||||
Err(e) => format!("Error writing to {}: {}", path, e),
|
||||
};
|
||||
(write_result, file_written)
|
||||
} else if name == "finish" {
|
||||
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
|
||||
let result = args.get("result").ok_or("Missing result argument")?;
|
||||
|
||||
println!("--- Finishing task: {}", result);
|
||||
|
||||
file_written = true;
|
||||
answer = Some(result.clone());
|
||||
(result.clone(), file_written)
|
||||
(result.clone(), true)
|
||||
} else {
|
||||
(format!("Error: Unknown tool {}", name), false)
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue