mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
* feat: add initial project structure and documentation - Introduced the GNU General Public License (GPL) v3 in LICENSE file. - Added MCP configuration in .claude/mcp.json for server integration. - Created architecture documentation in docs/ARCHITECTURE.md outlining the platform's design and components. - Defined MVP specifications in docs/MVP.md for the Telegram-based Agent Assistant. - Established API reference for team management in docs/teams-api-reference.md. - Set up basic HTML structure in public/index.html and added logo image in public/logo.png. * feat: add initial project documentation and HTML structure - Introduced CODE_OF_CONDUCT.md to establish community guidelines and standards for behavior. - Created CONTRIBUTING.md to outline contribution process, development setup, and project conventions. - Added SECURITY.md to define the security policy, supported versions, and reporting procedures for vulnerabilities. - Established basic HTML structure in index.html for the application interface. * chore: remove hello-python skill files - Deleted skill.json and skill.py files for the Hello Python example runtime skill, as they are no longer needed in the project. * feat: port tinyhuman agent runtime from ZeroClaw into Tauri backend Port daemon supervisor, health registry, security (policy, secrets, audit, pairing), agent traits, and config modules from ZeroClaw (MIT) into a new tinyhuman/ module under src-tauri/src/. The daemon auto-starts on desktop and shuts down gracefully on app exit via CancellationToken. - health: global HealthRegistry with component tracking and JSON snapshots - security/policy: SecurityPolicy with command validation, risk levels, rate limiting - security/secrets: ChaCha20-Poly1305 SecretStore with legacy XOR migration - security/audit: AuditLogger with JSON-line events and log rotation - security/pairing: PairingGuard with brute-force protection and SHA-256 hashing - security/traits: Sandbox trait + NoopSandbox - config: minimal DaemonConfig with autonomy, reliability, secrets, audit sub-configs - daemon: supervisor with health state writer emitting Tauri events - agent/traits: Provider, Tool, Memory, Observer, RuntimeAdapter traits + Noop impls - commands/tinyhuman: Tauri commands for health, security policy, encrypt/decrypt - 185 inline unit tests across all modules - README updated with custom inference/tunneling/memory positioning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update README to reflect AlphaHuman Mk1 branding and enhanced description - Changed project title to "AlphaHuman Mk1" for clarity. - Revised project description to emphasize user-friendly AI capabilities and the use of the Neocortex Mk1 model. - Removed outdated sections on custom inference, tunneling, and memory, streamlining the content for better readability. * update readme * Port zeroclaw runtime into tinyhuman * Replace CLI mentions with UI language * Split gateway module into smaller units * Split channels and config schema modules * Fix tinyhuman build, tests, and tunnel integration * feat(tinyhuman): add missing modules and ui-friendly services * refactor: rename tinyhuman to alphahuman * chore: remove bottom text from Welcome component * feat(settings): add tauri command console * feat(daemon): enhance daemon mode handling and integrate rustls with ring feature * feat(settings): implement comprehensive configuration management in TauriCommandsPanel * refactor(TauriCommandsPanel): streamline error handling and enhance async function usage * feat(settings): add skill management functionality to TauriCommandsPanel * style(TauriCommandsPanel): update input styles for improved readability and user experience * feat(settings): add Skills and Agent Chat panels with navigation and integration management * feat(settings): implement browser access management in SkillsPanel and enhance AgentChatPanel with local storage functionality --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
437 lines
11 KiB
Rust
437 lines
11 KiB
Rust
use crate::alphahuman::channels::{traits, Channel, SendMessage};
|
|
use crate::alphahuman::memory::{Memory, MemoryCategory, MemoryEntry};
|
|
use crate::alphahuman::providers::{ChatMessage, Provider};
|
|
use crate::alphahuman::tools::{Tool, ToolResult};
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
use tempfile::TempDir;
|
|
|
|
pub(super) fn make_workspace() -> TempDir {
|
|
let tmp = TempDir::new().unwrap();
|
|
// Create minimal workspace files
|
|
std::fs::write(tmp.path().join("SOUL.md"), "# Soul\nBe helpful.").unwrap();
|
|
std::fs::write(tmp.path().join("IDENTITY.md"), "# Identity\nName: Alphahuman").unwrap();
|
|
std::fs::write(tmp.path().join("USER.md"), "# User\nName: Test User").unwrap();
|
|
std::fs::write(
|
|
tmp.path().join("AGENTS.md"),
|
|
"# Agents\nFollow instructions.",
|
|
)
|
|
.unwrap();
|
|
std::fs::write(tmp.path().join("TOOLS.md"), "# Tools\nUse shell carefully.").unwrap();
|
|
std::fs::write(
|
|
tmp.path().join("HEARTBEAT.md"),
|
|
"# Heartbeat\nCheck status.",
|
|
)
|
|
.unwrap();
|
|
std::fs::write(tmp.path().join("MEMORY.md"), "# Memory\nUser likes Rust.").unwrap();
|
|
tmp
|
|
}
|
|
|
|
pub(super) struct DummyProvider;
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for DummyProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok("ok".to_string())
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct RecordingChannel {
|
|
pub(super) sent_messages: tokio::sync::Mutex<Vec<String>>,
|
|
pub(super) start_typing_calls: AtomicUsize,
|
|
pub(super) stop_typing_calls: AtomicUsize,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct TelegramRecordingChannel {
|
|
pub(super) sent_messages: tokio::sync::Mutex<Vec<String>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Channel for TelegramRecordingChannel {
|
|
fn name(&self) -> &str {
|
|
"telegram"
|
|
}
|
|
|
|
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
|
|
self.sent_messages
|
|
.lock()
|
|
.await
|
|
.push(format!("{}:{}", message.recipient, message.content));
|
|
Ok(())
|
|
}
|
|
|
|
async fn listen(
|
|
&self,
|
|
_tx: tokio::sync::mpsc::Sender<traits::ChannelMessage>,
|
|
) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Channel for RecordingChannel {
|
|
fn name(&self) -> &str {
|
|
"test-channel"
|
|
}
|
|
|
|
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
|
|
self.sent_messages
|
|
.lock()
|
|
.await
|
|
.push(format!("{}:{}", message.recipient, message.content));
|
|
Ok(())
|
|
}
|
|
|
|
async fn listen(
|
|
&self,
|
|
_tx: tokio::sync::mpsc::Sender<traits::ChannelMessage>,
|
|
) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {
|
|
self.start_typing_calls.fetch_add(1, Ordering::SeqCst);
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {
|
|
self.stop_typing_calls.fetch_add(1, Ordering::SeqCst);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(super) struct SlowProvider {
|
|
pub(super) delay: Duration,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for SlowProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
tokio::time::sleep(self.delay).await;
|
|
Ok(format!("echo: {message}"))
|
|
}
|
|
}
|
|
|
|
pub(super) struct ToolCallingProvider;
|
|
|
|
pub(super) fn tool_call_payload() -> String {
|
|
r#"<tool_call>
|
|
{"name":"mock_price","arguments":{"symbol":"BTC"}}
|
|
</tool_call>"#
|
|
.to_string()
|
|
}
|
|
|
|
pub(super) fn tool_call_payload_with_alias_tag() -> String {
|
|
r#"<toolcall>
|
|
{"name":"mock_price","arguments":{"symbol":"BTC"}}
|
|
</toolcall>"#
|
|
.to_string()
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for ToolCallingProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok(tool_call_payload())
|
|
}
|
|
|
|
async fn chat_with_history(
|
|
&self,
|
|
messages: &[ChatMessage],
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
let has_tool_results = messages
|
|
.iter()
|
|
.any(|msg| msg.role == "user" && msg.content.contains("[Tool results]"));
|
|
if has_tool_results {
|
|
Ok("BTC is currently around $65,000 based on latest tool output.".to_string())
|
|
} else {
|
|
Ok(tool_call_payload())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) struct ToolCallingAliasProvider;
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for ToolCallingAliasProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok(tool_call_payload_with_alias_tag())
|
|
}
|
|
|
|
async fn chat_with_history(
|
|
&self,
|
|
messages: &[ChatMessage],
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
let has_tool_results = messages
|
|
.iter()
|
|
.any(|msg| msg.role == "user" && msg.content.contains("[Tool results]"));
|
|
if has_tool_results {
|
|
Ok("BTC alias-tag flow resolved to final text output.".to_string())
|
|
} else {
|
|
Ok(tool_call_payload_with_alias_tag())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) struct IterativeToolProvider {
|
|
pub(super) required_tool_iterations: usize,
|
|
}
|
|
|
|
impl IterativeToolProvider {
|
|
pub(super) fn completed_tool_iterations(messages: &[ChatMessage]) -> usize {
|
|
messages
|
|
.iter()
|
|
.filter(|msg| msg.role == "user" && msg.content.contains("[Tool results]"))
|
|
.count()
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for IterativeToolProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok(tool_call_payload())
|
|
}
|
|
|
|
async fn chat_with_history(
|
|
&self,
|
|
messages: &[ChatMessage],
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
let completed_iterations = Self::completed_tool_iterations(messages);
|
|
if completed_iterations >= self.required_tool_iterations {
|
|
Ok(format!(
|
|
"Completed after {completed_iterations} tool iterations."
|
|
))
|
|
} else {
|
|
Ok(tool_call_payload())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct HistoryCaptureProvider {
|
|
pub(super) calls: Mutex<Vec<Vec<(String, String)>>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for HistoryCaptureProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok("fallback".to_string())
|
|
}
|
|
|
|
async fn chat_with_history(
|
|
&self,
|
|
messages: &[ChatMessage],
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
let snapshot = messages
|
|
.iter()
|
|
.map(|m| (m.role.clone(), m.content.clone()))
|
|
.collect::<Vec<_>>();
|
|
let mut calls = self.calls.lock().unwrap_or_else(|e| e.into_inner());
|
|
calls.push(snapshot);
|
|
Ok(format!("response-{}", calls.len()))
|
|
}
|
|
}
|
|
|
|
pub(super) struct MockPriceTool;
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct ModelCaptureProvider {
|
|
pub(super) call_count: AtomicUsize,
|
|
pub(super) models: Mutex<Vec<String>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Provider for ModelCaptureProvider {
|
|
async fn chat_with_system(
|
|
&self,
|
|
_system_prompt: Option<&str>,
|
|
_message: &str,
|
|
_model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
Ok("fallback".to_string())
|
|
}
|
|
|
|
async fn chat_with_history(
|
|
&self,
|
|
_messages: &[ChatMessage],
|
|
model: &str,
|
|
_temperature: f64,
|
|
) -> anyhow::Result<String> {
|
|
self.call_count.fetch_add(1, Ordering::SeqCst);
|
|
self.models
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.push(model.to_string());
|
|
Ok("ok".to_string())
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Tool for MockPriceTool {
|
|
fn name(&self) -> &str {
|
|
"mock_price"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Return a mocked BTC price"
|
|
}
|
|
|
|
fn parameters_schema(&self) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"symbol": { "type": "string" }
|
|
},
|
|
"required": ["symbol"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
|
let symbol = args.get("symbol").and_then(serde_json::Value::as_str);
|
|
if symbol != Some("BTC") {
|
|
return Ok(ToolResult {
|
|
success: false,
|
|
output: String::new(),
|
|
error: Some("unexpected symbol".to_string()),
|
|
});
|
|
}
|
|
|
|
Ok(ToolResult {
|
|
success: true,
|
|
output: "BTC is $65,000".to_string(),
|
|
error: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(super) struct NoopMemory;
|
|
|
|
#[async_trait::async_trait]
|
|
impl Memory for NoopMemory {
|
|
fn name(&self) -> &str {
|
|
"noop"
|
|
}
|
|
|
|
async fn store(
|
|
&self,
|
|
_key: &str,
|
|
_content: &str,
|
|
_category: MemoryCategory,
|
|
_session_id: Option<&str>,
|
|
) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn recall(
|
|
&self,
|
|
_query: &str,
|
|
_limit: usize,
|
|
_session_id: Option<&str>,
|
|
) -> anyhow::Result<Vec<MemoryEntry>> {
|
|
Ok(Vec::new())
|
|
}
|
|
|
|
async fn get(&self, _key: &str) -> anyhow::Result<Option<MemoryEntry>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn list(
|
|
&self,
|
|
_category: Option<&MemoryCategory>,
|
|
_session_id: Option<&str>,
|
|
) -> anyhow::Result<Vec<MemoryEntry>> {
|
|
Ok(Vec::new())
|
|
}
|
|
|
|
async fn forget(&self, _key: &str) -> anyhow::Result<bool> {
|
|
Ok(false)
|
|
}
|
|
|
|
async fn count(&self) -> anyhow::Result<usize> {
|
|
Ok(0)
|
|
}
|
|
|
|
async fn health_check(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
pub(super) struct AlwaysFailChannel {
|
|
pub(super) name: &'static str,
|
|
pub(super) calls: Arc<AtomicUsize>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Channel for AlwaysFailChannel {
|
|
fn name(&self) -> &str {
|
|
self.name
|
|
}
|
|
|
|
async fn send(&self, _message: &SendMessage) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn listen(
|
|
&self,
|
|
_tx: tokio::sync::mpsc::Sender<traits::ChannelMessage>,
|
|
) -> anyhow::Result<()> {
|
|
self.calls.fetch_add(1, Ordering::SeqCst);
|
|
anyhow::bail!("listen boom")
|
|
}
|
|
}
|