mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Extract fabro-interview crate from fabro-workflows
The interviewer module (trait + 7 implementations for human-in-the-loop interactions) had zero dependencies on fabro-workflows internals, making it a clean extraction. Consumers (fabro-api, fabro-slack) now depend on fabro-interview directly instead of reaching through fabro-workflows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4da08d269b
commit
c18062bc3f
31 changed files with 132 additions and 99 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -1286,6 +1286,7 @@ dependencies = [
|
|||
"fabro-exe",
|
||||
"fabro-github",
|
||||
"fabro-graphviz",
|
||||
"fabro-interview",
|
||||
"fabro-llm",
|
||||
"fabro-retro",
|
||||
"fabro-types",
|
||||
|
|
@ -1504,6 +1505,19 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-interview"
|
||||
version = "0.174.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dialoguer",
|
||||
"fabro-util",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-linear"
|
||||
version = "0.174.0"
|
||||
|
|
@ -1598,6 +1612,7 @@ dependencies = [
|
|||
name = "fabro-slack"
|
||||
version = "0.174.0"
|
||||
dependencies = [
|
||||
"fabro-interview",
|
||||
"fabro-workflows",
|
||||
"futures-util",
|
||||
"reqwest 0.12.28",
|
||||
|
|
@ -1721,7 +1736,6 @@ dependencies = [
|
|||
"clap",
|
||||
"cli-table",
|
||||
"console 0.15.11",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"fabro-agent",
|
||||
|
|
@ -1731,6 +1745,7 @@ dependencies = [
|
|||
"fabro-git-storage",
|
||||
"fabro-github",
|
||||
"fabro-graphviz",
|
||||
"fabro-interview",
|
||||
"fabro-llm",
|
||||
"fabro-mcp",
|
||||
"fabro-retro",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ doctest = false
|
|||
[dependencies]
|
||||
fabro-config = { path = "../fabro-config" }
|
||||
fabro-graphviz = { path = "../fabro-graphviz" }
|
||||
fabro-interview = { path = "../fabro-interview" }
|
||||
fabro-workflows = { path = "../fabro-workflows", features = ["exedev"] }
|
||||
fabro-daytona = { path = "../fabro-daytona" }
|
||||
fabro-github = { path = "../fabro-github" }
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ use fabro_config::server::ServerConfig;
|
|||
use crate::jwt_auth::{AuthMode, AuthStrategy};
|
||||
use crate::server::build_router;
|
||||
use crate::tls::ClientAuth;
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::cli::backend::AgentApiBackend;
|
||||
use fabro_workflows::cli::SandboxProvider;
|
||||
use fabro_workflows::handler::default_registry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ServeArgs {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,12 @@ use fabro_agent::LocalSandbox;
|
|||
|
||||
use crate::error::ApiError;
|
||||
use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser};
|
||||
use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer};
|
||||
use fabro_workflows::checkpoint::Checkpoint;
|
||||
use fabro_workflows::context::Context;
|
||||
use fabro_workflows::engine::{RunConfig, WorkflowRunEngine};
|
||||
use fabro_workflows::event::{EventEmitter, WorkflowRunEvent};
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::web::WebInterviewer;
|
||||
use fabro_workflows::interviewer::{Answer, Interviewer, QuestionType};
|
||||
|
||||
pub use fabro_types::{
|
||||
ApiQuestion, ApiQuestionOption, PaginatedRunList, PaginationMeta,
|
||||
|
|
@ -1474,9 +1473,7 @@ mod tests {
|
|||
start -> exit
|
||||
}"#;
|
||||
|
||||
fn test_registry(
|
||||
_interviewer: Arc<dyn fabro_workflows::interviewer::Interviewer>,
|
||||
) -> HandlerRegistry {
|
||||
fn test_registry(_interviewer: Arc<dyn fabro_interview::Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
|
|
|
|||
|
|
@ -433,9 +433,7 @@ mod tests {
|
|||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
|
||||
fn test_registry(
|
||||
_interviewer: Arc<dyn fabro_workflows::interviewer::Interviewer>,
|
||||
) -> HandlerRegistry {
|
||||
fn test_registry(_interviewer: Arc<dyn fabro_interview::Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ mod mtls_e2e {
|
|||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_api::server_config::TlsConfig;
|
||||
use fabro_api::tls::{build_rustls_config, ClientAuth};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::handler::agent::AgentHandler;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
fn simple_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
|
|
@ -431,12 +431,12 @@ mod server_lifecycle {
|
|||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::handler::agent::AgentHandler;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::human::HumanHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn gate_registry(interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
|
|
@ -627,11 +627,11 @@ mod sse_events {
|
|||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::handler::agent::AgentHandler;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
|
@ -789,8 +789,8 @@ mod serve_dry_run {
|
|||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::handler::default_registry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use tower::ServiceExt;
|
||||
|
||||
const MINIMAL_DOT: &str = r#"digraph Test {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ use fabro_api::jwt_auth::AuthMode;
|
|||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_api::server_config::*;
|
||||
use fabro_daytona::*;
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::cli::run_config::*;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::hook::*;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn test_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ use axum::body::Body;
|
|||
use axum::http::{Request, StatusCode};
|
||||
use fabro_api::jwt_auth::AuthMode;
|
||||
use fabro_api::server::{build_router, create_app_state};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::Interviewer;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn test_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
|
|
|
|||
21
lib/crates/fabro-interview/Cargo.toml
Normal file
21
lib/crates/fabro-interview/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "fabro-interview"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
description = "Human-in-the-loop interviewer traits and implementations"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
async-trait.workspace = true
|
||||
tokio.workspace = true
|
||||
dialoguer.workspace = true
|
||||
fabro-util = { path = "../fabro-util" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
tempfile = "3"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use super::{Answer, AnswerValue, Interviewer, Question, QuestionType};
|
||||
use crate::{Answer, AnswerValue, Interviewer, Question, QuestionType};
|
||||
|
||||
/// Always approves: YES for yes/no, first option for multiple choice, "auto-approved" for freeform.
|
||||
pub struct AutoApproveInterviewer;
|
||||
|
|
@ -29,7 +29,7 @@ impl Interviewer for AutoApproveInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::QuestionOption;
|
||||
use crate::QuestionOption;
|
||||
|
||||
#[tokio::test]
|
||||
async fn yes_no_returns_yes() {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use super::{Answer, Interviewer, Question};
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
/// Delegates question answering to a provided callback function.
|
||||
pub struct CallbackInterviewer {
|
||||
|
|
@ -25,7 +25,7 @@ impl Interviewer for CallbackInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::{AnswerValue, QuestionType};
|
||||
use crate::{AnswerValue, QuestionType};
|
||||
|
||||
#[tokio::test]
|
||||
async fn calls_callback_with_question() {
|
||||
|
|
@ -6,7 +6,7 @@ use dialoguer::theme::ColorfulTheme;
|
|||
use fabro_util::terminal::Styles;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
use super::{Answer, AnswerValue, Interviewer, Question, QuestionType};
|
||||
use crate::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType};
|
||||
|
||||
/// Reads from stdin to collect answers. Displays formatted prompts per spec 6.4.
|
||||
pub struct ConsoleInterviewer {
|
||||
|
|
@ -20,7 +20,7 @@ impl ConsoleInterviewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn find_matching_option(response: &str, options: &[super::QuestionOption]) -> Option<Answer> {
|
||||
fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option<Answer> {
|
||||
let trimmed = response.trim();
|
||||
// Try matching by key (case-insensitive)
|
||||
for opt in options {
|
||||
|
|
@ -228,11 +228,11 @@ mod tests {
|
|||
#[test]
|
||||
fn find_matching_option_by_key() {
|
||||
let options = vec![
|
||||
super::super::QuestionOption {
|
||||
crate::QuestionOption {
|
||||
key: "A".to_string(),
|
||||
label: "Approve".to_string(),
|
||||
},
|
||||
super::super::QuestionOption {
|
||||
crate::QuestionOption {
|
||||
key: "R".to_string(),
|
||||
label: "Reject".to_string(),
|
||||
},
|
||||
|
|
@ -245,7 +245,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn find_matching_option_by_key_case_insensitive() {
|
||||
let options = vec![super::super::QuestionOption {
|
||||
let options = vec![crate::QuestionOption {
|
||||
key: "Y".to_string(),
|
||||
label: "Yes".to_string(),
|
||||
}];
|
||||
|
|
@ -256,11 +256,11 @@ mod tests {
|
|||
#[test]
|
||||
fn find_matching_option_by_index() {
|
||||
let options = vec![
|
||||
super::super::QuestionOption {
|
||||
crate::QuestionOption {
|
||||
key: "A".to_string(),
|
||||
label: "Alpha".to_string(),
|
||||
},
|
||||
super::super::QuestionOption {
|
||||
crate::QuestionOption {
|
||||
key: "B".to_string(),
|
||||
label: "Beta".to_string(),
|
||||
},
|
||||
|
|
@ -273,7 +273,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn find_matching_option_no_match() {
|
||||
let options = vec![super::super::QuestionOption {
|
||||
let options = vec![crate::QuestionOption {
|
||||
key: "A".to_string(),
|
||||
label: "Alpha".to_string(),
|
||||
}];
|
||||
|
|
@ -283,7 +283,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn find_matching_option_index_out_of_range() {
|
||||
let options = vec![super::super::QuestionOption {
|
||||
let options = vec![crate::QuestionOption {
|
||||
key: "A".to_string(),
|
||||
label: "Alpha".to_string(),
|
||||
}];
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
pub mod auto_approve;
|
||||
pub mod callback;
|
||||
pub mod console;
|
||||
pub mod queue;
|
||||
pub mod recording;
|
||||
pub mod replay;
|
||||
pub mod web;
|
||||
mod auto_approve;
|
||||
mod callback;
|
||||
mod console;
|
||||
mod queue;
|
||||
mod recording;
|
||||
mod replay;
|
||||
mod web;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -199,6 +199,15 @@ pub trait Interviewer: Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
// Re-export all implementors at the crate root
|
||||
pub use auto_approve::AutoApproveInterviewer;
|
||||
pub use callback::CallbackInterviewer;
|
||||
pub use console::ConsoleInterviewer;
|
||||
pub use queue::QueueInterviewer;
|
||||
pub use recording::RecordingInterviewer;
|
||||
pub use replay::ReplayInterviewer;
|
||||
pub use web::{PendingQuestion, WebInterviewer};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -336,7 +345,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn ask_with_timeout_no_timeout_returns_normally() {
|
||||
let interviewer = crate::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
let interviewer = AutoApproveInterviewer;
|
||||
let q = Question::new("approve?", QuestionType::YesNo);
|
||||
|
||||
let answer = ask_with_timeout(&interviewer, q).await;
|
||||
|
|
@ -3,7 +3,7 @@ use std::sync::Mutex;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{Answer, Interviewer, Question};
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
/// Reads answers from a pre-filled queue. Returns Skipped when empty.
|
||||
pub struct QueueInterviewer {
|
||||
|
|
@ -30,7 +30,7 @@ impl Interviewer for QueueInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::{AnswerValue, QuestionType};
|
||||
use crate::{AnswerValue, QuestionType};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_queued_answers_in_order() {
|
||||
|
|
@ -3,8 +3,7 @@ use std::sync::Mutex;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{Answer, Interviewer, Question};
|
||||
use crate::error::FabroError;
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
/// Wraps another interviewer and records all question-answer pairs.
|
||||
pub struct RecordingInterviewer {
|
||||
|
|
@ -35,24 +34,26 @@ impl RecordingInterviewer {
|
|||
///
|
||||
/// # Errors
|
||||
/// Returns an error if serialization fails.
|
||||
pub fn to_json(&self) -> Result<String, FabroError> {
|
||||
pub fn to_json(&self) -> std::io::Result<String> {
|
||||
let recordings = self.recordings();
|
||||
serde_json::to_string_pretty(&recordings).map_err(|e| FabroError::Io(e.to_string()))
|
||||
serde_json::to_string_pretty(&recordings)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
/// Deserializes recordings from a JSON string.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if deserialization fails.
|
||||
pub fn from_json(json: &str) -> Result<Vec<(Question, Answer)>, FabroError> {
|
||||
serde_json::from_str(json).map_err(|e| FabroError::Io(e.to_string()))
|
||||
pub fn from_json(json: &str) -> std::io::Result<Vec<(Question, Answer)>> {
|
||||
serde_json::from_str(json)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
/// Saves recordings to a file as JSON.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if serialization or file writing fails.
|
||||
pub fn save_to_file(&self, path: &Path) -> Result<(), FabroError> {
|
||||
pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
|
||||
let json = self.to_json()?;
|
||||
std::fs::write(path, json)?;
|
||||
Ok(())
|
||||
|
|
@ -62,7 +63,7 @@ impl RecordingInterviewer {
|
|||
///
|
||||
/// # Errors
|
||||
/// Returns an error if file reading or deserialization fails.
|
||||
pub fn load_from_file(path: &Path) -> Result<Vec<(Question, Answer)>, FabroError> {
|
||||
pub fn load_from_file(path: &Path) -> std::io::Result<Vec<(Question, Answer)>> {
|
||||
let json = std::fs::read_to_string(path)?;
|
||||
Self::from_json(&json)
|
||||
}
|
||||
|
|
@ -83,8 +84,8 @@ impl Interviewer for RecordingInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
use crate::interviewer::{AnswerValue, QuestionType};
|
||||
use crate::AutoApproveInterviewer;
|
||||
use crate::{AnswerValue, QuestionType};
|
||||
|
||||
#[tokio::test]
|
||||
async fn records_question_answer_pairs() {
|
||||
|
|
@ -2,7 +2,7 @@ use std::sync::Mutex;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{Answer, Interviewer, Question};
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
/// Replays recorded answers in sequence. When recordings are exhausted,
|
||||
/// returns `Answer::skipped()`.
|
||||
|
|
@ -37,7 +37,7 @@ impl Interviewer for ReplayInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::{AnswerValue, QuestionType};
|
||||
use crate::{AnswerValue, QuestionType};
|
||||
|
||||
#[tokio::test]
|
||||
async fn replays_recorded_answers() {
|
||||
|
|
@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
|
|||
use async_trait::async_trait;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use super::{Answer, Interviewer, Question};
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
/// A pending question waiting for an answer from an external source (e.g., HTTP endpoint).
|
||||
#[derive(Debug)]
|
||||
|
|
@ -109,7 +109,7 @@ impl Interviewer for WebInterviewer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::{AnswerValue, QuestionType};
|
||||
use crate::{AnswerValue, QuestionType};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -169,7 +169,7 @@ mod tests {
|
|||
q.timeout_seconds = Some(0.05);
|
||||
|
||||
// Use ask_with_timeout from the parent module
|
||||
let answer = crate::interviewer::ask_with_timeout(interviewer.as_ref(), q).await;
|
||||
let answer = crate::ask_with_timeout(interviewer.as_ref(), q).await;
|
||||
assert_eq!(answer.value, AnswerValue::Timeout);
|
||||
}
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ description = "Slack Socket Mode integration for Fabro interviewer"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
fabro-interview = { path = "../fabro-interview" }
|
||||
fabro-workflows = { path = "../fabro-workflows" }
|
||||
futures-util.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_interview::{
|
||||
Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType, WebInterviewer,
|
||||
};
|
||||
use fabro_slack::blocks::{answered_blocks, question_to_blocks};
|
||||
use fabro_slack::client::{PostedMessage, SlackClient};
|
||||
use fabro_slack::connection;
|
||||
use fabro_slack::threads::ThreadRegistry;
|
||||
use fabro_workflows::interviewer::web::WebInterviewer;
|
||||
use fabro_workflows::interviewer::{
|
||||
Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType,
|
||||
};
|
||||
|
||||
struct TestCase {
|
||||
label: &'static str,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use fabro_workflows::interviewer::{Question, QuestionType};
|
||||
use fabro_interview::{Question, QuestionType};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn text_block(text: &str) -> Value {
|
||||
|
|
@ -95,7 +95,7 @@ pub fn question_to_blocks(question_id: &str, question: &Question) -> Vec<Value>
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fabro_workflows::interviewer::QuestionOption;
|
||||
use fabro_interview::QuestionOption;
|
||||
|
||||
#[test]
|
||||
fn yes_no_produces_two_buttons() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_workflows::interviewer::web::WebInterviewer;
|
||||
use fabro_interview::WebInterviewer;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
|
@ -187,7 +187,7 @@ pub async fn run(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fabro_workflows::interviewer::AnswerValue;
|
||||
use fabro_interview::AnswerValue;
|
||||
|
||||
fn registry() -> ThreadRegistry {
|
||||
ThreadRegistry::new()
|
||||
|
|
@ -304,7 +304,7 @@ mod tests {
|
|||
let i_clone = Arc::clone(&interviewer);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
use fabro_workflows::interviewer::{Interviewer, Question, QuestionType};
|
||||
use fabro_interview::{Interviewer, Question, QuestionType};
|
||||
let q = Question::new("approve?", QuestionType::YesNo);
|
||||
i_clone.ask(q).await
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use fabro_workflows::interviewer::Answer;
|
||||
use fabro_interview::Answer;
|
||||
|
||||
use crate::interaction;
|
||||
use crate::socket::{classify_envelope, SocketEnvelope, SocketEventKind};
|
||||
|
|
@ -50,7 +50,7 @@ pub fn dispatch(envelope: &SocketEnvelope, thread_registry: &ThreadRegistry) ->
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fabro_workflows::interviewer::AnswerValue;
|
||||
use fabro_interview::AnswerValue;
|
||||
|
||||
#[test]
|
||||
fn hello_produces_connected() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use fabro_workflows::interviewer::Answer;
|
||||
use fabro_interview::Answer;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Parses a Slack interaction payload and returns (question_id, Answer).
|
||||
|
|
@ -63,7 +63,7 @@ fn extract_checkbox_selections(question_id: &str, payload: &Value) -> Answer {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fabro_workflows::interviewer::AnswerValue;
|
||||
use fabro_interview::AnswerValue;
|
||||
|
||||
#[test]
|
||||
fn parse_yes_button_click() {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ fabro-exe = { path = "../fabro-exe", optional = true }
|
|||
fabro-ssh = { path = "../fabro-ssh" }
|
||||
fabro-mcp = { path = "../fabro-mcp" }
|
||||
fabro-github = { path = "../fabro-github" }
|
||||
fabro-interview = { path = "../fabro-interview" }
|
||||
fabro-util = { path = "../fabro-util" }
|
||||
fabro-daytona = { path = "../fabro-daytona" }
|
||||
fabro-git-storage = { path = "../fabro-git-storage" }
|
||||
|
|
@ -45,7 +46,6 @@ futures.workspace = true
|
|||
chrono = { workspace = true, features = ["serde"] }
|
||||
toml.workspace = true
|
||||
dirs = "6"
|
||||
dialoguer.workspace = true
|
||||
regex.workspace = true
|
||||
scopeguard = "1"
|
||||
hex.workspace = true
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ use console::Style;
|
|||
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
use crate::interviewer::{Answer, Interviewer, Question};
|
||||
use crate::outcome::StageStatus;
|
||||
use fabro_agent::AgentEvent;
|
||||
use fabro_interview::{Answer, ConsoleInterviewer, Interviewer, Question};
|
||||
|
||||
use super::{compute_stage_cost, format_cost, format_tokens_human};
|
||||
|
||||
|
|
@ -1266,15 +1266,12 @@ impl ProgressUI {
|
|||
/// Wraps a `ConsoleInterviewer` so that progress bars are hidden during
|
||||
/// interactive prompts (avoids garbled output from concurrent writes).
|
||||
pub struct ProgressAwareInterviewer {
|
||||
inner: crate::interviewer::console::ConsoleInterviewer,
|
||||
inner: ConsoleInterviewer,
|
||||
progress: Arc<Mutex<ProgressUI>>,
|
||||
}
|
||||
|
||||
impl ProgressAwareInterviewer {
|
||||
pub fn new(
|
||||
inner: crate::interviewer::console::ConsoleInterviewer,
|
||||
progress: Arc<Mutex<ProgressUI>>,
|
||||
) -> Self {
|
||||
pub fn new(inner: ConsoleInterviewer, progress: Arc<Mutex<ProgressUI>>) -> Self {
|
||||
Self { inner, progress }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ use crate::checkpoint::Checkpoint;
|
|||
use crate::engine::{RunConfig, WorkflowRunEngine};
|
||||
use crate::event::EventEmitter;
|
||||
use crate::handler::default_registry;
|
||||
use crate::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
use crate::interviewer::console::ConsoleInterviewer;
|
||||
use crate::interviewer::Interviewer;
|
||||
use crate::outcome::StageStatus;
|
||||
use crate::workflow::WorkflowBuilder;
|
||||
use fabro_interview::{AutoApproveInterviewer, ConsoleInterviewer, Interviewer};
|
||||
use fabro_validate::Severity;
|
||||
|
||||
use fabro_llm::provider::Provider;
|
||||
|
|
@ -1708,10 +1706,10 @@ async fn run_from_branch(
|
|||
};
|
||||
|
||||
// Build interviewer
|
||||
let interviewer: Arc<dyn crate::interviewer::Interviewer> = if args.auto_approve {
|
||||
Arc::new(crate::interviewer::auto_approve::AutoApproveInterviewer)
|
||||
let interviewer: Arc<dyn fabro_interview::Interviewer> = if args.auto_approve {
|
||||
Arc::new(fabro_interview::AutoApproveInterviewer)
|
||||
} else {
|
||||
Arc::new(crate::interviewer::console::ConsoleInterviewer::new(styles))
|
||||
Arc::new(fabro_interview::ConsoleInterviewer::new(styles))
|
||||
};
|
||||
|
||||
// Build engine with a backend
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ use crate::error::{FabroError, FailureClass, FailureSignature, Result};
|
|||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
use crate::handler::{EngineServices, HandlerRegistry};
|
||||
use crate::hook::{HookContext, HookDecision, HookEvent, HookRunner};
|
||||
use crate::interviewer::Interviewer;
|
||||
use crate::millis_u64;
|
||||
use crate::outcome::{Outcome, StageStatus};
|
||||
use crate::preamble::build_preamble;
|
||||
use fabro_graphviz::graph::{Edge, Graph, Node};
|
||||
use fabro_interview::Interviewer;
|
||||
|
||||
/// Classify the failure mode of a completed outcome.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ use crate::context::keys;
|
|||
use crate::context::Context;
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
use crate::interviewer::{
|
||||
Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType,
|
||||
};
|
||||
use crate::millis_u64;
|
||||
use crate::outcome::Outcome;
|
||||
use fabro_graphviz::graph::{Graph, Node};
|
||||
use fabro_interview::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType};
|
||||
|
||||
use super::{EngineServices, Handler};
|
||||
|
||||
|
|
@ -327,9 +325,8 @@ fn answer_text(answer: &Answer) -> String {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
use crate::interviewer::recording::RecordingInterviewer;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge};
|
||||
use fabro_interview::{AutoApproveInterviewer, RecordingInterviewer};
|
||||
|
||||
fn make_services() -> EngineServices {
|
||||
EngineServices::test_default()
|
||||
|
|
@ -437,9 +434,9 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn wait_human_with_freeform_edge() {
|
||||
let interviewer = Arc::new(crate::interviewer::callback::CallbackInterviewer::new(
|
||||
|_| Answer::text("custom input"),
|
||||
));
|
||||
let interviewer = Arc::new(fabro_interview::CallbackInterviewer::new(|_| {
|
||||
Answer::text("custom input")
|
||||
}));
|
||||
let handler = HumanHandler::new(interviewer);
|
||||
|
||||
let mut graph = Graph::new("test");
|
||||
|
|
@ -474,9 +471,9 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn freeform_only_gate_uses_freeform_question_type() {
|
||||
let inner = Box::new(crate::interviewer::callback::CallbackInterviewer::new(
|
||||
|_| Answer::text("hello"),
|
||||
));
|
||||
let inner = Box::new(fabro_interview::CallbackInterviewer::new(|_| {
|
||||
Answer::text("hello")
|
||||
}));
|
||||
let recorder = Arc::new(RecordingInterviewer::new(inner));
|
||||
let handler = HumanHandler::new(recorder.clone());
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ use crate::engine::GitState;
|
|||
use crate::error::FabroError;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::hook::{HookContext, HookDecision, HookRunner};
|
||||
use crate::interviewer::Interviewer;
|
||||
use crate::outcome::Outcome;
|
||||
use fabro_graphviz::graph::{shape_to_handler_type, Graph, Node};
|
||||
use fabro_interview::Interviewer;
|
||||
|
||||
/// Shared services available to all handlers during execution.
|
||||
pub struct EngineServices {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ pub mod event;
|
|||
pub mod git;
|
||||
pub mod handler;
|
||||
pub mod hook;
|
||||
pub mod interviewer;
|
||||
pub mod manifest;
|
||||
pub mod outcome;
|
||||
pub mod preamble;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ use std::time::Duration;
|
|||
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_graphviz::parser::parse;
|
||||
use fabro_interview::{
|
||||
Answer, AnswerValue, AutoApproveInterviewer, Interviewer, QueueInterviewer,
|
||||
RecordingInterviewer,
|
||||
};
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_validate::{validate, validate_or_raise, Severity};
|
||||
use fabro_workflows::checkpoint::Checkpoint;
|
||||
|
|
@ -23,10 +27,6 @@ use fabro_workflows::handler::manager_loop::SubWorkflowHandler;
|
|||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::wait::WaitHandler;
|
||||
use fabro_workflows::handler::{Handler, HandlerRegistry};
|
||||
use fabro_workflows::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
use fabro_workflows::interviewer::queue::QueueInterviewer;
|
||||
use fabro_workflows::interviewer::recording::RecordingInterviewer;
|
||||
use fabro_workflows::interviewer::{Answer, AnswerValue, Interviewer};
|
||||
use fabro_workflows::outcome::{Outcome, StageStatus};
|
||||
use fabro_workflows::stylesheet::{apply_stylesheet, parse_stylesheet};
|
||||
use fabro_workflows::transform::{
|
||||
|
|
@ -6106,6 +6106,7 @@ mod real_llm {
|
|||
|
||||
use super::local_env;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph};
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_workflows::checkpoint::Checkpoint;
|
||||
use fabro_workflows::engine::{RunConfig, WorkflowRunEngine};
|
||||
use fabro_workflows::event::EventEmitter;
|
||||
|
|
@ -6113,7 +6114,6 @@ mod real_llm {
|
|||
use fabro_workflows::handler::human::HumanHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue