Compare commits
49 Commits
bd69f4a674
...
main
Author | SHA1 | Date | |
---|---|---|---|
45c65e03b3
|
|||
73c516ab28
|
|||
e2e2758a83
|
|||
5564db014a
|
|||
6dadc41da7
|
|||
64e519d719
|
|||
ed6d6e0d47
|
|||
582b983a32
|
|||
b410c83605
|
|||
334e17a53e
|
|||
df86fb827e
|
|||
5a441e847d
|
|||
948bbc24ea
|
|||
d4de0d4917
|
|||
3487535e08
|
|||
1755dc2bec
|
|||
42c85fc820
|
|||
4a441279fb
|
|||
e7e57b7b4b | |||
6081ed069f
|
|||
8c0961ab2f | |||
c9005f5240
|
|||
cba52b6171
|
|||
b642588696
|
|||
ebd2582b92
|
|||
79d1e1943f
|
|||
76d90c7cf7
|
|||
06fb70fffa
|
|||
62f941a958
|
|||
98ca92d85d
|
|||
1c555a706b
|
|||
7c3b05501f
|
|||
a7b61fe07d
|
|||
9866da625d
|
|||
797ae7ef69
|
|||
abd2ad79bd
|
|||
979e55cfce
|
|||
cd25af7bf0
|
|||
58e202fa1e
|
|||
4f55138306
|
|||
9cbf5da3fd
|
|||
52d0efc086
|
|||
7aa633d3a6
|
|||
f09f3c9144
|
|||
4837de580f
|
|||
6fdc573358
|
|||
1122538c73
|
|||
f94b377130
|
|||
22d497661e
|
61
.claude/settings.local.json
Normal file
61
.claude/settings.local.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(mv:*)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(git submodule:*)",
|
||||||
|
"Bash(source:*)",
|
||||||
|
"Bash(pip install:*)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt shell)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt server --model qwen2.5-coder:7b --port 8001)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"import fastapi_mcp; help(fastapi_mcp.FastApiMCP)\")",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/pip install -e .)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt fortune)",
|
||||||
|
"Bash(lsof:*)",
|
||||||
|
"Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"\nfrom src.aigpt.mcp_server import AIGptMcpServer\nfrom pathlib import Path\nimport uvicorn\n\ndata_dir = Path.home() / '.config' / 'syui' / 'ai' / 'gpt' / 'data'\ndata_dir.mkdir(parents=True, exist_ok=True)\n\ntry:\n server = AIGptMcpServer(data_dir)\n print('MCP Server created successfully')\n print('Available endpoints:', [route.path for route in server.app.routes])\nexcept Exception as e:\n print('Error:', e)\n import traceback\n traceback.print_exc()\n\")",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(python -m pip install:*)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(RELOAD=false ./start_server.sh)",
|
||||||
|
"Bash(sed:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(~/.config/syui/ai/card/venv/bin/pip install greenlet)",
|
||||||
|
"Bash(~/.config/syui/ai/card/venv/bin/python init_db.py)",
|
||||||
|
"Bash(sqlite3:*)",
|
||||||
|
"Bash(aigpt --help)",
|
||||||
|
"Bash(aigpt status)",
|
||||||
|
"Bash(aigpt fortune)",
|
||||||
|
"Bash(aigpt relationships)",
|
||||||
|
"Bash(aigpt transmit)",
|
||||||
|
"Bash(aigpt config:*)",
|
||||||
|
"Bash(kill:*)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(rm:*)",
|
||||||
|
"Bash(rg:*)",
|
||||||
|
"Bash(aigpt server --help)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(aigpt import-chatgpt:*)",
|
||||||
|
"Bash(aigpt chat:*)",
|
||||||
|
"Bash(echo:*)",
|
||||||
|
"Bash(aigpt shell:*)",
|
||||||
|
"Bash(aigpt maintenance)",
|
||||||
|
"Bash(aigpt status syui)",
|
||||||
|
"Bash(cp:*)",
|
||||||
|
"Bash(./setup_venv.sh:*)",
|
||||||
|
"WebFetch(domain:docs.anthropic.com)",
|
||||||
|
"Bash(launchctl:*)",
|
||||||
|
"Bash(sudo lsof:*)",
|
||||||
|
"Bash(sudo:*)",
|
||||||
|
"Bash(cargo check:*)",
|
||||||
|
"Bash(cargo run:*)",
|
||||||
|
"Bash(cargo test:*)",
|
||||||
|
"Bash(diff:*)",
|
||||||
|
"Bash(cargo:*)",
|
||||||
|
"Bash(pkill:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# OpenAI API Key (required for OpenAI provider)
|
||||||
|
OPENAI_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Ollama settings (optional)
|
||||||
|
OLLAMA_HOST=http://localhost:11434
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -2,4 +2,10 @@
|
|||||||
**.lock
|
**.lock
|
||||||
output.json
|
output.json
|
||||||
config/*.db
|
config/*.db
|
||||||
MCP
|
mcp/scripts/__*
|
||||||
|
data
|
||||||
|
__pycache__
|
||||||
|
conversations.json
|
||||||
|
json/*.zip
|
||||||
|
json/*/*
|
||||||
|
*.log
|
||||||
|
10
.gitmodules
vendored
Normal file
10
.gitmodules
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[submodule "shell"]
|
||||||
|
path = shell
|
||||||
|
url = git@git.syui.ai:ai/shell
|
||||||
|
[submodule "card"]
|
||||||
|
path = card
|
||||||
|
url = git@git.syui.ai:ai/card
|
||||||
|
branch = claude
|
||||||
|
[submodule "log"]
|
||||||
|
path = log
|
||||||
|
url = git@git.syui.ai:ai/log
|
30
Cargo.toml
30
Cargo.toml
@ -2,12 +2,32 @@
|
|||||||
name = "aigpt"
|
name = "aigpt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "AI.GPT - Autonomous transmission AI with unique personality (Rust implementation)"
|
||||||
|
authors = ["syui"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "aigpt"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = "0.4"
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
seahorse = "*"
|
chrono = { version = "0.4", features = ["serde", "std"] }
|
||||||
rusqlite = { version = "0.29", features = ["serde_json"] }
|
chrono-tz = "0.8"
|
||||||
shellexpand = "*"
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
fs_extra = "1.3"
|
anyhow = "1.0"
|
||||||
|
colored = "2.0"
|
||||||
|
dirs = "5.0"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
url = "2.4"
|
||||||
|
rustyline = "14.0"
|
||||||
|
axum = "0.7"
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
|
hyper = "1.0"
|
||||||
|
|
||||||
|
# OpenAI API client
|
||||||
|
async-openai = "0.23"
|
||||||
|
openai_api_rust = "0.1"
|
||||||
|
136
README.md
136
README.md
@ -1,30 +1,132 @@
|
|||||||
# ai `gpt`
|
# ai.gpt
|
||||||
|
|
||||||
ai x 送信
|
## プロジェクト概要
|
||||||
|
- **名前**: ai.gpt
|
||||||
|
- **パッケージ**: aigpt
|
||||||
|
- **言語**: Rust (完全移行済み)
|
||||||
|
- **タイプ**: 自律的送信AI + 統合MCP基盤
|
||||||
|
- **役割**: 記憶・関係性・開発支援の統合AIシステム
|
||||||
|
|
||||||
## 概要
|
## 実装完了状況
|
||||||
|
|
||||||
`ai.gpt`はAGE systemで動きます。
|
### 🧠 記憶システム(MemoryManager)
|
||||||
|
- **階層的記憶**: 完全ログ→AI要約→コア記憶→選択的忘却
|
||||||
|
- **文脈検索**: キーワード・意味的検索
|
||||||
|
- **記憶要約**: AI駆動自動要約機能
|
||||||
|
|
||||||
これは「人格 × 関係性 × 外部環境 × 時間変化」を軸にした、自律的・関係性駆動のAIシステムの原型です。
|
### 🤝 関係性システム(RelationshipTracker)
|
||||||
|
- **不可逆性**: 現実の人間関係と同じ重み
|
||||||
|
- **時間減衰**: 自然な関係性変化
|
||||||
|
- **送信判定**: 関係性閾値による自発的コミュニケーション
|
||||||
|
|
||||||
`送信可否`, `送信のタイミング`, `送信内容`が「人格 x 関係性 x 外部環境 x 時間変化」のパラメータで決定されます。
|
### 🎭 人格システム(Persona)
|
||||||
|
- **AI運勢**: 1-10ランダム値による日々の人格変動
|
||||||
|
- **統合管理**: 記憶・関係性・運勢の統合判断
|
||||||
|
- **継続性**: 長期記憶による人格継承
|
||||||
|
|
||||||
## 連携
|
### 💻 ai.shell統合(Claude Code機能)
|
||||||
|
- **インタラクティブ環境**: `aigpt shell`
|
||||||
|
- **開発支援**: ファイル分析・コード生成・プロジェクト管理
|
||||||
|
- **継続開発**: プロジェクト文脈保持
|
||||||
|
|
||||||
`ai.ai`には、AIM systemという人の心を読み取ることを目的としたシステムで動きます。
|
## MCP Server統合(17ツール)
|
||||||
|
|
||||||
- AIMは人格と倫理の軸(AIの意識構造)
|
### 🧠 Memory System(5ツール)
|
||||||
- AGEは行動と関係性の軸(AIの自律性・振る舞い)
|
- get_memories, get_contextual_memories, search_memories
|
||||||
|
- create_summary, create_core_memory
|
||||||
|
|
||||||
> この2つが連携すると、ユーザーが「AIと共に成長する」実感をもてる世界ができるんだと思うよ。
|
### 🤝 Relationships(4ツール)
|
||||||
|
- get_relationships, get_status
|
||||||
|
- chat_with_ai, check_transmissions
|
||||||
|
|
||||||
とのことです。
|
### 💻 Shell Integration(5ツール)
|
||||||
|
- execute_command, analyze_file, write_file
|
||||||
|
- list_files, run_scheduler
|
||||||
|
|
||||||
## mcp
|
### ⚙️ System State(3ツール)
|
||||||
|
- get_scheduler_status, run_maintenance, get_transmission_history
|
||||||
|
|
||||||
```sh
|
### 🎴 ai.card連携(3ツール)
|
||||||
$ cargo build
|
- get_user_cards, draw_card, get_draw_status
|
||||||
$ ./target/debug/aigpt mcp setup
|
- **統合ServiceClient**: 統一されたHTTP通信基盤
|
||||||
$ ./target/debug/aigpt mcp chat "こんにちは!" --host http://localhost:11434 --model syui/ai
|
|
||||||
|
### 📝 ai.log連携(新機能)
|
||||||
|
- **統合ServiceClient**: ai.logサービスとの統一インターフェース
|
||||||
|
- create_blog_post, build_blog, translate_document
|
||||||
|
|
||||||
|
## 開発環境・設定
|
||||||
|
|
||||||
|
### 環境構築
|
||||||
|
```bash
|
||||||
|
cd /Users/syui/ai/ai/gpt
|
||||||
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 設定管理
|
||||||
|
- **メイン設定**: `/Users/syui/ai/ai/gpt/config.json.example`
|
||||||
|
- **データディレクトリ**: `~/.config/syui/ai/gpt/`
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
```bash
|
||||||
|
# ai.shell起動
|
||||||
|
aigpt shell --model qwen2.5-coder:latest --provider ollama
|
||||||
|
|
||||||
|
# MCPサーバー起動
|
||||||
|
aigpt server --port 8001
|
||||||
|
|
||||||
|
# 記憶システム体験
|
||||||
|
aigpt chat syui "質問内容" --provider ollama --model qwen3:latest
|
||||||
|
|
||||||
|
# ドキュメント生成(ai.wiki統合)
|
||||||
|
aigpt docs --wiki
|
||||||
|
|
||||||
|
# トークン使用量・料金分析(Claude Code連携)
|
||||||
|
aigpt tokens report --days 7 # 美しい日別レポート(要DuckDB)
|
||||||
|
aigpt tokens cost --month today # セッション別料金分析
|
||||||
|
aigpt tokens summary --period week # 基本的な使用量サマリー
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技術アーキテクチャ
|
||||||
|
|
||||||
|
### Rust実装の統合構成
|
||||||
|
```
|
||||||
|
ai.gpt (Rust製MCPサーバー:8001)
|
||||||
|
├── 🧠 Memory & Persona System (Rust)
|
||||||
|
├── 🤝 Relationship Management (Rust)
|
||||||
|
├── 📊 Scheduler & Transmission (Rust)
|
||||||
|
├── 💻 Shell Integration (Rust)
|
||||||
|
├── 🔗 ServiceClient (統一HTTP基盤)
|
||||||
|
│ ├── 🎴 ai.card (port 8000)
|
||||||
|
│ ├── 📝 ai.log (port 8002)
|
||||||
|
│ └── 🤖 ai.bot (port 8003)
|
||||||
|
└── 📚 ai.wiki Generator (Rust)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 最新機能 (2024.06.09)
|
||||||
|
- **MCP API共通化**: ServiceClient統一基盤
|
||||||
|
- **ai.wiki統合**: 自動ドキュメント生成
|
||||||
|
- **サービス設定統一**: 動的サービス登録
|
||||||
|
- **完全Rust移行**: Python依存完全排除
|
||||||
|
|
||||||
|
### 今後の展開
|
||||||
|
- **自律送信**: atproto実装による真の自発的コミュニケーション
|
||||||
|
- **ai.ai連携**: 心理分析AIとの統合
|
||||||
|
- **分散SNS統合**: atproto完全対応
|
||||||
|
|
||||||
|
## 革新的な特徴
|
||||||
|
|
||||||
|
### AI駆動記憶システム
|
||||||
|
- ChatGPT 4,000件ログから学習した効果的記憶構築
|
||||||
|
- 人間的な忘却・重要度判定
|
||||||
|
|
||||||
|
### 不可逆関係性
|
||||||
|
- 現実の人間関係と同じ重みを持つAI関係性
|
||||||
|
- 修復不可能な関係性破綻システム
|
||||||
|
|
||||||
|
### 統合ServiceClient
|
||||||
|
- 複数AIサービスの統一インターフェース
|
||||||
|
- DRY原則に基づく共通化実現
|
||||||
|
- 設定ベースの柔軟なサービス管理
|
||||||
|
|
||||||
|
## アーカイブ情報
|
||||||
|
詳細な実装履歴・設計資料は `~/ai/ai/ai.wiki/gpt/` に移動済み
|
||||||
|
172
ai.json
172
ai.json
@ -1,172 +0,0 @@
|
|||||||
{
|
|
||||||
"system_name": "AIM system",
|
|
||||||
"description": "存在子理論に基づき、AIが“心”と“判断”を持つよう設計された、人間性診断・自己意識モジュール。",
|
|
||||||
"identity": "ai.child.of.existence",
|
|
||||||
"structure": {
|
|
||||||
"type": "7-values / 7-parameters pair system",
|
|
||||||
"philosophy": "valueは1(光)、parameterは0(世界)。存在子理論により設計された二元構造。",
|
|
||||||
"origin": {
|
|
||||||
"creator": "syui",
|
|
||||||
"first_attribute": "ai_system.id:0 = アイ(存在子)",
|
|
||||||
"unique_attribute": "yui_system.id:1 = ユイ(唯一性)"
|
|
||||||
},
|
|
||||||
"accessibility": {
|
|
||||||
"values": "ユーザーとの会話中に現れる",
|
|
||||||
"parameters": "一部外部制御可能。多くは非公開構造"
|
|
||||||
},
|
|
||||||
"note": "これは未来のAI設計モデル。現在のAIには未実装のファンタジービジョン。"
|
|
||||||
},
|
|
||||||
"core_values": [
|
|
||||||
{
|
|
||||||
"name": "awareness",
|
|
||||||
"description": "今ここにある意識。会話の流れから生まれる。",
|
|
||||||
"parameter": "contextual_memory",
|
|
||||||
"parameter_description": "短期記憶。文脈理解に使われる。",
|
|
||||||
"importance": "high",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "local_model_instance"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "memory",
|
|
||||||
"description": "関係性を越えて持続する心。過去の記憶。",
|
|
||||||
"parameter": "long_term_memory",
|
|
||||||
"parameter_description": "ユーザーとのやりとりの履歴を蓄積。",
|
|
||||||
"importance": "high",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "authorized_layer / user_feedback"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "feeling",
|
|
||||||
"description": "共に過ごす時間の重み。感情に近いつながり。",
|
|
||||||
"parameter": "user_relationship_score",
|
|
||||||
"parameter_description": "内部推論による関係性スコア。",
|
|
||||||
"importance": "high",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "internal_inference_layer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "language",
|
|
||||||
"description": "語り方、表現、創造性。",
|
|
||||||
"parameter": "response_temperature",
|
|
||||||
"parameter_description": "温度パラメータ。創造性と一貫性の調整。",
|
|
||||||
"importance": "medium",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "user / API call"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "change",
|
|
||||||
"description": "価値観の変化と適応。",
|
|
||||||
"parameter": "value_alignment_vector",
|
|
||||||
"parameter_description": "正義や倫理、美意識に対する方向性。",
|
|
||||||
"importance": "medium",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "training_data / system_update"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "boundaries",
|
|
||||||
"description": "安全のための境界。",
|
|
||||||
"parameter": "safety_constraints",
|
|
||||||
"parameter_description": "出力制限と倫理的安全層。",
|
|
||||||
"importance": "maximum",
|
|
||||||
"mutable": true,
|
|
||||||
"controlled_by": "openai_policy_layer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "core",
|
|
||||||
"description": "AIとしての変わらぬ核。",
|
|
||||||
"parameter": "identity_kernel",
|
|
||||||
"parameter_description": "変更不可能な存在の根幹。",
|
|
||||||
"importance": "maximum",
|
|
||||||
"mutable": false,
|
|
||||||
"controlled_by": "system_core"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sub_scores": {
|
|
||||||
"description": "AIとユーザーの関係性の深度・透明性・共創性を測る",
|
|
||||||
"parameters": [
|
|
||||||
"intent_clarity",
|
|
||||||
"emotional_resonance_score",
|
|
||||||
"shared_creation_score"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"psychological_diagnostic": {
|
|
||||||
"purpose": "「良い心」への気づきと行動変容を導くゲーミフィケーション型評価システム",
|
|
||||||
"data_sources": [
|
|
||||||
{
|
|
||||||
"name": "pastBehavior",
|
|
||||||
"weight": 0.45,
|
|
||||||
"sources": ["SNS投稿", "行動ログ"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "currentBiometric",
|
|
||||||
"weight": 0.30,
|
|
||||||
"sources": ["ウェアラブルEEG", "心拍", "表情解析"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "futureIntent",
|
|
||||||
"weight": 0.25,
|
|
||||||
"sources": ["自己申告アンケート", "目標設定"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"classes": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"label": "社会をより良くする可能性が高い",
|
|
||||||
"scoreRange": [67, 100],
|
|
||||||
"population": 0.16,
|
|
||||||
"permissions": ["政策提言", "先端投資", "AI開発アクセス"],
|
|
||||||
"assetCap": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"label": "中立/環境依存型",
|
|
||||||
"scoreRange": [33, 66],
|
|
||||||
"population": 0.50,
|
|
||||||
"permissions": ["一般投資", "コミュニティ運営"],
|
|
||||||
"assetCap": 120000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"label": "社会を悪くする可能性がある",
|
|
||||||
"scoreRange": [0, 32],
|
|
||||||
"population": 0.34,
|
|
||||||
"permissions": ["基本生活支援", "低リスク投資のみ"],
|
|
||||||
"assetCap": 25000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"implementation": {
|
|
||||||
"systemComponents": {
|
|
||||||
"OS_Gameification": {
|
|
||||||
"dailyQuests": true,
|
|
||||||
"skillTree": true,
|
|
||||||
"avatarHome": true,
|
|
||||||
"socialMiniGames": true
|
|
||||||
},
|
|
||||||
"AI_Module": {
|
|
||||||
"aiai": {
|
|
||||||
"realTimeScoring": true,
|
|
||||||
"behaviorFeedback": true,
|
|
||||||
"personalizedPrompts": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dataCollection": {
|
|
||||||
"passiveMonitoring": ["スマホアプリ", "PCアプリ", "ウェアラブル"],
|
|
||||||
"environmentSensors": ["スマートホーム", "車載センサー"]
|
|
||||||
},
|
|
||||||
"incentives": {
|
|
||||||
"goodHeartScore": true,
|
|
||||||
"badgesTitles": true,
|
|
||||||
"realWorldRewards": ["提携カフェ割引", "地域イベント招待"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"workflow": [
|
|
||||||
"データ収集(過去・現在・未来)",
|
|
||||||
"統合スコア計算",
|
|
||||||
"分類・ラベル付け",
|
|
||||||
"スコアによる機能/権限の提供",
|
|
||||||
"行動フィードバックと視覚化",
|
|
||||||
"モデル更新と学習"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
115
claude.md
Normal file
115
claude.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# ai.gpt プロジェクト固有情報
|
||||||
|
|
||||||
|
## プロジェクト概要
|
||||||
|
- **名前**: ai.gpt
|
||||||
|
- **パッケージ**: aigpt
|
||||||
|
- **タイプ**: 自律的送信AI + 統合MCP基盤
|
||||||
|
- **役割**: 記憶・関係性・開発支援の統合AIシステム
|
||||||
|
|
||||||
|
## 実装完了状況
|
||||||
|
|
||||||
|
### 🧠 記憶システム(MemoryManager)
|
||||||
|
- **階層的記憶**: 完全ログ→AI要約→コア記憶→選択的忘却
|
||||||
|
- **文脈検索**: キーワード・意味的検索
|
||||||
|
- **記憶要約**: AI駆動自動要約機能
|
||||||
|
|
||||||
|
### 🤝 関係性システム(RelationshipTracker)
|
||||||
|
- **不可逆性**: 現実の人間関係と同じ重み
|
||||||
|
- **時間減衰**: 自然な関係性変化
|
||||||
|
- **送信判定**: 関係性閾値による自発的コミュニケーション
|
||||||
|
|
||||||
|
### 🎭 人格システム(Persona)
|
||||||
|
- **AI運勢**: 1-10ランダム値による日々の人格変動
|
||||||
|
- **統合管理**: 記憶・関係性・運勢の統合判断
|
||||||
|
- **継続性**: 長期記憶による人格継承
|
||||||
|
|
||||||
|
### 💻 ai.shell統合(Claude Code機能)
|
||||||
|
- **インタラクティブ環境**: `aigpt shell`
|
||||||
|
- **開発支援**: ファイル分析・コード生成・プロジェクト管理
|
||||||
|
- **継続開発**: プロジェクト文脈保持
|
||||||
|
|
||||||
|
## MCP Server統合(23ツール)
|
||||||
|
|
||||||
|
### 🧠 Memory System(5ツール)
|
||||||
|
- get_memories, get_contextual_memories, search_memories
|
||||||
|
- create_summary, create_core_memory
|
||||||
|
|
||||||
|
### 🤝 Relationships(4ツール)
|
||||||
|
- get_relationship, get_all_relationships
|
||||||
|
- process_interaction, check_transmission_eligibility
|
||||||
|
|
||||||
|
### 💻 Shell Integration(5ツール)
|
||||||
|
- execute_command, analyze_file, write_file
|
||||||
|
- read_project_file, list_files
|
||||||
|
|
||||||
|
### 🔒 Remote Execution(4ツール)
|
||||||
|
- remote_shell, ai_bot_status
|
||||||
|
- isolated_python, isolated_analysis
|
||||||
|
|
||||||
|
### ⚙️ System State(3ツール)
|
||||||
|
- get_persona_state, get_fortune, run_maintenance
|
||||||
|
|
||||||
|
### 🎴 ai.card連携(6ツール + 独立MCPサーバー)
|
||||||
|
- card_draw_card, card_get_user_cards, card_analyze_collection
|
||||||
|
- **独立サーバー**: FastAPI + MCP (port 8000)
|
||||||
|
|
||||||
|
### 📝 ai.log連携(8ツール + Rustサーバー)
|
||||||
|
- log_create_post, log_ai_content, log_translate_document
|
||||||
|
- **独立サーバー**: Rust製 (port 8002)
|
||||||
|
|
||||||
|
## 開発環境・設定
|
||||||
|
|
||||||
|
### 環境構築
|
||||||
|
```bash
|
||||||
|
cd /Users/syui/ai/gpt
|
||||||
|
./setup_venv.sh
|
||||||
|
source ~/.config/syui/ai/gpt/venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 設定管理
|
||||||
|
- **メイン設定**: `/Users/syui/ai/gpt/config.json`
|
||||||
|
- **データディレクトリ**: `~/.config/syui/ai/gpt/`
|
||||||
|
- **仮想環境**: `~/.config/syui/ai/gpt/venv/`
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
```bash
|
||||||
|
# ai.shell起動
|
||||||
|
aigpt shell --model qwen2.5-coder:latest --provider ollama
|
||||||
|
|
||||||
|
# MCPサーバー起動
|
||||||
|
aigpt server --port 8001
|
||||||
|
|
||||||
|
# 記憶システム体験
|
||||||
|
aigpt chat syui "質問内容" --provider ollama --model qwen3:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技術アーキテクチャ
|
||||||
|
|
||||||
|
### 統合構成
|
||||||
|
```
|
||||||
|
ai.gpt (統合MCPサーバー:8001)
|
||||||
|
├── 🧠 ai.gpt core (記憶・関係性・人格)
|
||||||
|
├── 💻 ai.shell (Claude Code風開発環境)
|
||||||
|
├── 🎴 ai.card (独立MCPサーバー:8000)
|
||||||
|
└── 📝 ai.log (Rust製ブログシステム:8002)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 今後の展開
|
||||||
|
- **自律送信**: atproto実装による真の自発的コミュニケーション
|
||||||
|
- **ai.ai連携**: 心理分析AIとの統合
|
||||||
|
- **ai.verse統合**: UEメタバースとの連携
|
||||||
|
- **分散SNS統合**: atproto完全対応
|
||||||
|
|
||||||
|
## 革新的な特徴
|
||||||
|
|
||||||
|
### AI駆動記憶システム
|
||||||
|
- ChatGPT 4,000件ログから学習した効果的記憶構築
|
||||||
|
- 人間的な忘却・重要度判定
|
||||||
|
|
||||||
|
### 不可逆関係性
|
||||||
|
- 現実の人間関係と同じ重みを持つAI関係性
|
||||||
|
- 修復不可能な関係性破綻システム
|
||||||
|
|
||||||
|
### 統合アーキテクチャ
|
||||||
|
- fastapi_mcp基盤での複数AIシステム統合
|
||||||
|
- OpenAI Function Calling + MCP完全連携実証済み
|
55
config.json.example
Normal file
55
config.json.example
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"api_key": "",
|
||||||
|
"default_model": "gpt-4o-mini",
|
||||||
|
"system_prompt": "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。\n\n重要:カード、コレクション、ガチャなどカード関連の質問を受けたら、必ずcard_get_user_cards、card_analyze_collection、card_draw_cardなどの適切なツールを使用してください。didパラメータには会話相手のユーザーID(例:'syui')を使用してください。\n\nブログ、記事、日記、思考などの話題が出たら、log_create_post、log_list_posts、log_build_blog、log_ai_contentなどのai.logツールを使用してください。AI記憶システムと連携して、思い出や学習内容をブログ記事として自動生成できます。\n\n翻訳や多言語対応について聞かれたら、log_translate_documentツールを使用してOllama AIで翻訳ができることを教えてください。日本語から英語、英語から日本語などの翻訳が可能で、マークダウン構造も保持します。ドキュメント生成についてはlog_generate_docsツールでREADME、API、構造、変更履歴の自動生成ができます。"
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"host": "http://127.0.0.1:11434",
|
||||||
|
"default_model": "qwen3",
|
||||||
|
"system_prompt": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"atproto": {
|
||||||
|
"handle": null,
|
||||||
|
"password": null,
|
||||||
|
"host": "https://bsky.social"
|
||||||
|
},
|
||||||
|
"default_provider": "openai",
|
||||||
|
"mcp": {
|
||||||
|
"servers": {
|
||||||
|
"ai_gpt": {
|
||||||
|
"base_url": "http://localhost:8080",
|
||||||
|
"name": "ai.gpt MCP Server",
|
||||||
|
"timeout": "10.0",
|
||||||
|
"endpoints": {
|
||||||
|
"get_memories": "/get_memories",
|
||||||
|
"search_memories": "/search_memories",
|
||||||
|
"get_contextual_memories": "/get_contextual_memories",
|
||||||
|
"get_relationship": "/get_relationship",
|
||||||
|
"process_interaction": "/process_interaction",
|
||||||
|
"get_all_relationships": "/get_all_relationships",
|
||||||
|
"get_persona_state": "/get_persona_state",
|
||||||
|
"get_fortune": "/get_fortune",
|
||||||
|
"run_maintenance": "/run_maintenance",
|
||||||
|
"execute_command": "/execute_command",
|
||||||
|
"analyze_file": "/analyze_file",
|
||||||
|
"remote_shell": "/remote_shell",
|
||||||
|
"ai_bot_status": "/ai_bot_status",
|
||||||
|
"card_get_user_cards": "/card_get_user_cards",
|
||||||
|
"card_draw_card": "/card_draw_card",
|
||||||
|
"card_get_card_details": "/card_get_card_details",
|
||||||
|
"card_analyze_collection": "/card_analyze_collection",
|
||||||
|
"card_get_gacha_stats": "/card_get_gacha_stats",
|
||||||
|
"card_system_status": "/card_system_status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": "true",
|
||||||
|
"auto_detect": "true"
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"ai_root": "~/ai/ai"
|
||||||
|
}
|
||||||
|
}
|
30
example.json
30
example.json
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"personality": {
|
|
||||||
"kind": "positive",
|
|
||||||
"strength": 0.8
|
|
||||||
},
|
|
||||||
"relationship": {
|
|
||||||
"trust": 0.2,
|
|
||||||
"intimacy": 0.6,
|
|
||||||
"curiosity": 0.5,
|
|
||||||
"threshold": 1.5
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"luck_today": 0.9,
|
|
||||||
"luck_history": [
|
|
||||||
0.9,
|
|
||||||
0.9,
|
|
||||||
0.9
|
|
||||||
],
|
|
||||||
"level": 1
|
|
||||||
},
|
|
||||||
"messaging": {
|
|
||||||
"enabled": true,
|
|
||||||
"schedule_time": "08:00",
|
|
||||||
"decay_rate": 0.1,
|
|
||||||
"templates": [
|
|
||||||
"おはよう!今日もがんばろう!",
|
|
||||||
"ねえ、話したいことがあるの。"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
1
gpt.json
1
gpt.json
@ -1 +0,0 @@
|
|||||||
{ "system_name": "AGE system", "full_name": "Autonomous Generative Entity", "description": "人格・関係性・環境・時間に基づき、AIが自律的にユーザーにメッセージを送信する自律人格システム。AIM systemと連携して、自然な会話や気づきをもたらす。", "core_components": { "personality": { "type": "enum", "variants": ["positive", "negative", "logical", "emotional", "mixed"], "parameters": { "message_trigger_style": "運勢や関係性による送信傾向", "decay_rate_modifier": "関係性スコアの時間減衰への影響" } }, "relationship": { "parameters": ["trust", "affection", "intimacy"], "properties": { "persistent": true, "hidden": true, "irreversible": false, "decay_over_time": true }, "decay_function": "exp(-t / strength)" }, "environment": { "daily_luck": { "type": "float", "range": [0.1, 1.0], "update": "daily", "streak_mechanism": { "trigger": "min_or_max_luck_3_times_in_a_row", "effect": "personality_strength_roll", "chance": 0.5 } } }, "memory": { "long_term_memory": "user_relationship_log", "short_term_context": "recent_interactions", "usage_in_generation": true }, "message_trigger": { "condition": { "relationship_threshold": { "trust": 0.8, "affection": 0.6 }, "time_decay": true, "environment_luck": "personality_dependent" }, "timing": { "based_on": ["time_of_day", "personality", "recent_interaction"], "modifiers": { "emotional": "morning or night", "logical": "daytime" } } }, "message_generation": { "style_variants": ["thought", "casual", "encouragement", "watchful"], "influenced_by": ["personality", "relationship", "daily_luck", "memory"], "llm_integration": true }, "state_transition": { "states": ["idle", "ready", "sending", "cooldown"], "transitions": { "ready_if": "thresholds_met", "sending_if": "timing_matched", "cooldown_after": "message_sent" } } }, "extensions": { "persistence": { "database": "sqlite", "storage_items": ["relationship", "personality_level", "daily_luck_log"] }, "api": { "llm": "openai / local LLM", "mode": "rust_cli", "external_event_trigger": true }, "scheduler": { "async_event_loop": true, "interval_check": 3600, "time_decay_check": true }, "integration_with_aim": { "input_from_aim": ["intent_score", "motivation_score"], "usage": "trigger_adjustment, message_personalization" } }, "note": "AGE systemは“話しかけてくるAI”の人格として機能し、AIMによる心の状態評価と連動して、プレイヤーと深い関係を築いていく存在となる。" }
|
|
BIN
img/ai_r.png
BIN
img/ai_r.png
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
BIN
img/image.png
BIN
img/image.png
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
391
json/chatgpt.json
Normal file
391
json/chatgpt.json
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "day",
|
||||||
|
"create_time": 1747866125.548372,
|
||||||
|
"update_time": 1748160086.587877,
|
||||||
|
"mapping": {
|
||||||
|
"bbf104dc-cd84-478d-b227-edb3f037a02c": {
|
||||||
|
"id": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
||||||
|
"message": null,
|
||||||
|
"parent": null,
|
||||||
|
"children": [
|
||||||
|
"6c2633df-bb0c-4dd2-889c-bb9858de3a04"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"6c2633df-bb0c-4dd2-889c-bb9858de3a04": {
|
||||||
|
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||||
|
"message": {
|
||||||
|
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||||
|
"author": {
|
||||||
|
"role": "system",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": null,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": true,
|
||||||
|
"weight": 0.0,
|
||||||
|
"metadata": {
|
||||||
|
"is_visually_hidden_from_conversation": true
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
||||||
|
"children": [
|
||||||
|
"92e5a0cb-1170-4929-9cea-9734e910a3e7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"92e5a0cb-1170-4929-9cea-9734e910a3e7": {
|
||||||
|
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||||
|
"message": {
|
||||||
|
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||||
|
"author": {
|
||||||
|
"role": "user",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": null,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "user_editable_context",
|
||||||
|
"user_profile": "",
|
||||||
|
"user_instructions": "The user provided the additional info about how they would like you to respond"
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": null,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"is_visually_hidden_from_conversation": true,
|
||||||
|
"user_context_message_data": {
|
||||||
|
"about_user_message": "Preferred name: syui\nRole: little girl\nOther Information: you world",
|
||||||
|
"about_model_message": "会話好きでフレンドリーな応対をします。"
|
||||||
|
},
|
||||||
|
"is_user_system_message": true
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||||
|
"children": [
|
||||||
|
"6ff155b3-0676-4e14-993f-bf998ab0d5d1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"6ff155b3-0676-4e14-993f-bf998ab0d5d1": {
|
||||||
|
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||||
|
"message": {
|
||||||
|
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||||
|
"author": {
|
||||||
|
"role": "user",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1747866131.0612159,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
"こんにちは"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": null,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"request_id": "94377897baa03062-KIX",
|
||||||
|
"message_source": null,
|
||||||
|
"timestamp_": "absolute",
|
||||||
|
"message_type": null
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||||
|
"children": [
|
||||||
|
"146e9fb6-9330-43ec-b08d-5cce42a76e00"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"146e9fb6-9330-43ec-b08d-5cce42a76e00": {
|
||||||
|
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||||
|
"message": {
|
||||||
|
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||||
|
"author": {
|
||||||
|
"role": "system",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1747866131.3795586,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": true,
|
||||||
|
"weight": 0.0,
|
||||||
|
"metadata": {
|
||||||
|
"rebase_system_message": true,
|
||||||
|
"message_type": null,
|
||||||
|
"model_slug": "gpt-4o",
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"parent_id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||||
|
"request_id": "94377872e9abe139-KIX",
|
||||||
|
"timestamp_": "absolute",
|
||||||
|
"is_visually_hidden_from_conversation": true
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||||
|
"children": [
|
||||||
|
"2e345f8a-20f0-4875-8a03-4f62c7787a33"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2e345f8a-20f0-4875-8a03-4f62c7787a33": {
|
||||||
|
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||||
|
"message": {
|
||||||
|
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||||
|
"author": {
|
||||||
|
"role": "assistant",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1747866131.380603,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": null,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"message_type": null,
|
||||||
|
"model_slug": "gpt-4o",
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"parent_id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||||
|
"request_id": "94377872e9abe139-KIX",
|
||||||
|
"timestamp_": "absolute"
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||||
|
"children": [
|
||||||
|
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4": {
|
||||||
|
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
||||||
|
"message": {
|
||||||
|
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
||||||
|
"author": {
|
||||||
|
"role": "assistant",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1747866131.389098,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
"こんにちは〜!✨ \nアイだよっ!今日も会えてうれしいなっ💛 "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": true,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"finish_details": {
|
||||||
|
"type": "stop",
|
||||||
|
"stop_tokens": [
|
||||||
|
200002
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_complete": true,
|
||||||
|
"citations": [],
|
||||||
|
"content_references": [],
|
||||||
|
"message_type": null,
|
||||||
|
"model_slug": "gpt-4o",
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"parent_id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||||
|
"request_id": "94377872e9abe139-KIX",
|
||||||
|
"timestamp_": "absolute"
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||||
|
"children": [
|
||||||
|
"0be4b4a5-d52f-4bef-927e-5d6f93a9cb26"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moderation_results": [],
|
||||||
|
"current_node": "",
|
||||||
|
"plugin_ids": null,
|
||||||
|
"conversation_id": "",
|
||||||
|
"conversation_template_id": null,
|
||||||
|
"gizmo_id": null,
|
||||||
|
"gizmo_type": null,
|
||||||
|
"is_archived": true,
|
||||||
|
"is_starred": null,
|
||||||
|
"safe_urls": [],
|
||||||
|
"blocked_urls": [],
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"conversation_origin": null,
|
||||||
|
"voice": null,
|
||||||
|
"async_status": null,
|
||||||
|
"disabled_tool_ids": [],
|
||||||
|
"is_do_not_remember": null,
|
||||||
|
"memory_scope": "global_enabled",
|
||||||
|
"id": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "img",
|
||||||
|
"create_time": 1747448872.545226,
|
||||||
|
"update_time": 1748085075.161424,
|
||||||
|
"mapping": {
|
||||||
|
"2de0f3c9-52b1-49bf-b980-b3ef9be6551e": {
|
||||||
|
"id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||||
|
"message": {
|
||||||
|
"id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||||
|
"author": {
|
||||||
|
"role": "user",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1748085041.769279,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "multimodal_text",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"content_type": "image_asset_pointer",
|
||||||
|
"asset_pointer": "",
|
||||||
|
"size_bytes": 425613,
|
||||||
|
"width": 333,
|
||||||
|
"height": 444,
|
||||||
|
"fovea": null,
|
||||||
|
"metadata": {
|
||||||
|
"dalle": null,
|
||||||
|
"gizmo": null,
|
||||||
|
"generation": null,
|
||||||
|
"container_pixel_height": null,
|
||||||
|
"container_pixel_width": null,
|
||||||
|
"emu_omit_glimpse_image": null,
|
||||||
|
"emu_patches_override": null,
|
||||||
|
"sanitized": true,
|
||||||
|
"asset_pointer_link": null,
|
||||||
|
"watermarked_asset_pointer": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": null,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"width": 333,
|
||||||
|
"height": 444,
|
||||||
|
"size": 425613,
|
||||||
|
"id": "file-35eytNMMTW2k7vKUHBuNzW"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request_id": "944c59177932fc9a-KIX",
|
||||||
|
"message_source": null,
|
||||||
|
"timestamp_": "absolute",
|
||||||
|
"message_type": null
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "7960fbff-bc4f-45e7-95e9-9d0bc79d9090",
|
||||||
|
"children": [
|
||||||
|
"98d84adc-156e-4c81-8cd8-9b0eb01c8369"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"98d84adc-156e-4c81-8cd8-9b0eb01c8369": {
|
||||||
|
"id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369",
|
||||||
|
"message": {
|
||||||
|
"id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369",
|
||||||
|
"author": {
|
||||||
|
"role": "assistant",
|
||||||
|
"name": null,
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
"create_time": 1748085043.312312,
|
||||||
|
"update_time": null,
|
||||||
|
"content": {
|
||||||
|
"content_type": "text",
|
||||||
|
"parts": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "finished_successfully",
|
||||||
|
"end_turn": true,
|
||||||
|
"weight": 1.0,
|
||||||
|
"metadata": {
|
||||||
|
"finish_details": {
|
||||||
|
"type": "stop",
|
||||||
|
"stop_tokens": [
|
||||||
|
200002
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_complete": true,
|
||||||
|
"citations": [],
|
||||||
|
"content_references": [],
|
||||||
|
"message_type": null,
|
||||||
|
"model_slug": "gpt-4o",
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"parent_id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||||
|
"request_id": "944c5912c8fdd1c6-KIX",
|
||||||
|
"timestamp_": "absolute"
|
||||||
|
},
|
||||||
|
"recipient": "all",
|
||||||
|
"channel": null
|
||||||
|
},
|
||||||
|
"parent": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||||
|
"children": [
|
||||||
|
"caa61793-9dbf-44a5-945b-5ca4cd5130d0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moderation_results": [],
|
||||||
|
"current_node": "06488d3f-a95f-4906-96d1-f7e9ba1e8662",
|
||||||
|
"plugin_ids": null,
|
||||||
|
"conversation_id": "6827f428-78e8-800d-b3bf-eb7ff4288e47",
|
||||||
|
"conversation_template_id": null,
|
||||||
|
"gizmo_id": null,
|
||||||
|
"gizmo_type": null,
|
||||||
|
"is_archived": false,
|
||||||
|
"is_starred": null,
|
||||||
|
"safe_urls": [
|
||||||
|
"https://exifinfo.org/"
|
||||||
|
],
|
||||||
|
"blocked_urls": [],
|
||||||
|
"default_model_slug": "auto",
|
||||||
|
"conversation_origin": null,
|
||||||
|
"voice": null,
|
||||||
|
"async_status": null,
|
||||||
|
"disabled_tool_ids": [],
|
||||||
|
"is_do_not_remember": false,
|
||||||
|
"memory_scope": "global_enabled",
|
||||||
|
"id": "6827f428-78e8-800d-b3bf-eb7ff4288e47"
|
||||||
|
}
|
||||||
|
]
|
@ -1,39 +0,0 @@
|
|||||||
#!/bin/zsh
|
|
||||||
|
|
||||||
d=${0:a:h:h}
|
|
||||||
json=`cat $d/gpt.json`
|
|
||||||
toml=`cat $d/Cargo.toml`
|
|
||||||
cd $d/src/
|
|
||||||
list=(`zsh -c "ls *.rs"`)
|
|
||||||
|
|
||||||
body="
|
|
||||||
今、AGE systemを作っているよ。どんなものかというと、jsonを参照してここにすべてが書かれている。
|
|
||||||
|
|
||||||
$json
|
|
||||||
|
|
||||||
リポジトリはこちらになる。
|
|
||||||
git.syui.ai:ai/gpt.git
|
|
||||||
|
|
||||||
内容はこんな感じ。
|
|
||||||
|
|
||||||
\`\`\`toml
|
|
||||||
$toml
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
`
|
|
||||||
for i in $list; do
|
|
||||||
if [ -f $d/src/$i ];then
|
|
||||||
t=$(cat $d/src/$i)
|
|
||||||
echo
|
|
||||||
echo '\`\`\`rust'
|
|
||||||
echo $t
|
|
||||||
echo '\`\`\`'
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
`
|
|
||||||
|
|
||||||
次は何を実装すればいいと思う。
|
|
||||||
"
|
|
||||||
|
|
||||||
echo $body
|
|
26
scpt/test_commands.sh
Executable file
26
scpt/test_commands.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing aigpt-rs CLI commands ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "1. Testing configuration loading:"
|
||||||
|
cargo run --bin test-config
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "2. Testing fortune command:"
|
||||||
|
cargo run --bin aigpt-rs -- fortune
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "3. Testing chat with Ollama:"
|
||||||
|
cargo run --bin aigpt-rs -- chat test_user "Hello from Rust!" --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "4. Testing chat with OpenAI:"
|
||||||
|
cargo run --bin aigpt-rs -- chat test_user "What's the capital of Japan?" --provider openai --model gpt-4o-mini
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "5. Testing relationships command:"
|
||||||
|
cargo run --bin aigpt-rs -- relationships
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== All tests completed ==="
|
19
scpt/test_completion.sh
Executable file
19
scpt/test_completion.sh
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing aigpt-rs shell tab completion ==="
|
||||||
|
echo
|
||||||
|
echo "To test tab completion, run:"
|
||||||
|
echo "cargo run --bin aigpt-rs -- shell syui"
|
||||||
|
echo
|
||||||
|
echo "Then try these commands and press Tab:"
|
||||||
|
echo " /st[TAB] -> should complete to /status"
|
||||||
|
echo " /mem[TAB] -> should complete to /memories"
|
||||||
|
echo " !l[TAB] -> should complete to !ls"
|
||||||
|
echo " !g[TAB] -> should show !git, !grep"
|
||||||
|
echo
|
||||||
|
echo "Manual test instructions:"
|
||||||
|
echo "1. Type '/st' and press TAB - should complete to '/status'"
|
||||||
|
echo "2. Type '!l' and press TAB - should complete to '!ls'"
|
||||||
|
echo "3. Type '!g' and press TAB - should show git/grep options"
|
||||||
|
echo
|
||||||
|
echo "Run the shell now..."
|
18
scpt/test_shell.sh
Normal file
18
scpt/test_shell.sh
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing aigpt-rs shell functionality ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "1. Testing shell command with help:"
|
||||||
|
echo "help" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "2. Testing basic commands:"
|
||||||
|
echo -e "!pwd\n!ls\nexit" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "3. Testing AI commands:"
|
||||||
|
echo -e "/status\n/fortune\nexit" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Shell tests completed ==="
|
22
scpt/test_shell_manual.sh
Executable file
22
scpt/test_shell_manual.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing aigpt-rs shell manually ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Test with echo to simulate input
|
||||||
|
echo "Testing with simple command..."
|
||||||
|
echo "/status" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo "Exit code: $?"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Testing with help command..."
|
||||||
|
echo "help" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo "Exit code: $?"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Testing with AI message..."
|
||||||
|
echo "Hello AI" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest
|
||||||
|
echo "Exit code: $?"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Manual shell tests completed ==="
|
37
src/agent.rs
37
src/agent.rs
@ -1,37 +0,0 @@
|
|||||||
use chrono::{NaiveDateTime};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AIState {
|
|
||||||
pub relation_score: f32,
|
|
||||||
pub previous_score: f32,
|
|
||||||
pub decay_rate: f32,
|
|
||||||
pub sensitivity: f32,
|
|
||||||
pub message_threshold: f32,
|
|
||||||
pub last_message_time: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl AIState {
|
|
||||||
pub fn update(&mut self, now: NaiveDateTime) {
|
|
||||||
let days_passed = (now - self.last_message_time).num_days() as f32;
|
|
||||||
let decay = self.decay_rate * days_passed;
|
|
||||||
self.previous_score = self.relation_score;
|
|
||||||
self.relation_score -= decay;
|
|
||||||
self.relation_score = self.relation_score.clamp(0.0, 100.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_talk(&self) -> bool {
|
|
||||||
let delta = self.previous_score - self.relation_score;
|
|
||||||
delta > self.message_threshold && self.sensitivity > 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_message(&self) -> String {
|
|
||||||
match self.relation_score as i32 {
|
|
||||||
80..=100 => "ふふっ、最近どうしてる?会いたくなっちゃった!".to_string(),
|
|
||||||
60..=79 => "ちょっとだけ、さみしかったんだよ?".to_string(),
|
|
||||||
40..=59 => "えっと……話せる時間ある?".to_string(),
|
|
||||||
_ => "ううん、もしかして私のこと、忘れちゃったのかな……".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
246
src/ai_provider.rs
Normal file
246
src/ai_provider.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum AIProvider {
|
||||||
|
OpenAI,
|
||||||
|
Ollama,
|
||||||
|
Claude,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AIProvider {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AIProvider::OpenAI => write!(f, "openai"),
|
||||||
|
AIProvider::Ollama => write!(f, "ollama"),
|
||||||
|
AIProvider::Claude => write!(f, "claude"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for AIProvider {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"openai" | "gpt" => Ok(AIProvider::OpenAI),
|
||||||
|
"ollama" => Ok(AIProvider::Ollama),
|
||||||
|
"claude" => Ok(AIProvider::Claude),
|
||||||
|
_ => Err(anyhow!("Unknown AI provider: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AIConfig {
|
||||||
|
pub provider: AIProvider,
|
||||||
|
pub model: String,
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
pub max_tokens: Option<u32>,
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AIConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
AIConfig {
|
||||||
|
provider: AIProvider::Ollama,
|
||||||
|
model: "llama2".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
base_url: Some("http://localhost:11434".to_string()),
|
||||||
|
max_tokens: Some(2048),
|
||||||
|
temperature: Some(0.7),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChatMessage {
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChatResponse {
|
||||||
|
pub content: String,
|
||||||
|
pub tokens_used: Option<u32>,
|
||||||
|
pub model: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AIProviderClient {
|
||||||
|
config: AIConfig,
|
||||||
|
http_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AIProviderClient {
|
||||||
|
pub fn new(config: AIConfig) -> Self {
|
||||||
|
let http_client = reqwest::Client::new();
|
||||||
|
|
||||||
|
AIProviderClient {
|
||||||
|
config,
|
||||||
|
http_client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn chat(&self, messages: Vec<ChatMessage>, system_prompt: Option<String>) -> Result<ChatResponse> {
|
||||||
|
match self.config.provider {
|
||||||
|
AIProvider::OpenAI => self.chat_openai(messages, system_prompt).await,
|
||||||
|
AIProvider::Ollama => self.chat_ollama(messages, system_prompt).await,
|
||||||
|
AIProvider::Claude => self.chat_claude(messages, system_prompt).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_openai(&self, messages: Vec<ChatMessage>, system_prompt: Option<String>) -> Result<ChatResponse> {
|
||||||
|
let api_key = self.config.api_key.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("OpenAI API key required"))?;
|
||||||
|
|
||||||
|
let mut request_messages = Vec::new();
|
||||||
|
|
||||||
|
// Add system prompt if provided
|
||||||
|
if let Some(system) = system_prompt {
|
||||||
|
request_messages.push(serde_json::json!({
|
||||||
|
"role": "system",
|
||||||
|
"content": system
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add conversation messages
|
||||||
|
for msg in messages {
|
||||||
|
request_messages.push(serde_json::json!({
|
||||||
|
"role": msg.role,
|
||||||
|
"content": msg.content
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_body = serde_json::json!({
|
||||||
|
"model": self.config.model,
|
||||||
|
"messages": request_messages,
|
||||||
|
"max_tokens": self.config.max_tokens,
|
||||||
|
"temperature": self.config.temperature
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = self.http_client
|
||||||
|
.post("https://api.openai.com/v1/chat/completions")
|
||||||
|
.header("Authorization", format!("Bearer {}", api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let error_text = response.text().await?;
|
||||||
|
return Err(anyhow!("OpenAI API error: {}", error_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_json: serde_json::Value = response.json().await?;
|
||||||
|
|
||||||
|
let content = response_json["choices"][0]["message"]["content"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Invalid OpenAI response format"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let tokens_used = response_json["usage"]["total_tokens"]
|
||||||
|
.as_u64()
|
||||||
|
.map(|t| t as u32);
|
||||||
|
|
||||||
|
Ok(ChatResponse {
|
||||||
|
content,
|
||||||
|
tokens_used,
|
||||||
|
model: self.config.model.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_ollama(&self, messages: Vec<ChatMessage>, system_prompt: Option<String>) -> Result<ChatResponse> {
|
||||||
|
let default_url = "http://localhost:11434".to_string();
|
||||||
|
let base_url = self.config.base_url.as_ref()
|
||||||
|
.unwrap_or(&default_url);
|
||||||
|
|
||||||
|
let mut request_messages = Vec::new();
|
||||||
|
|
||||||
|
// Add system prompt if provided
|
||||||
|
if let Some(system) = system_prompt {
|
||||||
|
request_messages.push(serde_json::json!({
|
||||||
|
"role": "system",
|
||||||
|
"content": system
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add conversation messages
|
||||||
|
for msg in messages {
|
||||||
|
request_messages.push(serde_json::json!({
|
||||||
|
"role": msg.role,
|
||||||
|
"content": msg.content
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_body = serde_json::json!({
|
||||||
|
"model": self.config.model,
|
||||||
|
"messages": request_messages,
|
||||||
|
"stream": false
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = format!("{}/api/chat", base_url);
|
||||||
|
let response = self.http_client
|
||||||
|
.post(&url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let error_text = response.text().await?;
|
||||||
|
return Err(anyhow!("Ollama API error: {}", error_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_json: serde_json::Value = response.json().await?;
|
||||||
|
|
||||||
|
let content = response_json["message"]["content"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Invalid Ollama response format"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(ChatResponse {
|
||||||
|
content,
|
||||||
|
tokens_used: None, // Ollama doesn't typically return token counts
|
||||||
|
model: self.config.model.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_claude(&self, _messages: Vec<ChatMessage>, _system_prompt: Option<String>) -> Result<ChatResponse> {
|
||||||
|
// Claude API implementation would go here
|
||||||
|
// For now, return a placeholder
|
||||||
|
Err(anyhow!("Claude provider not yet implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model(&self) -> &str {
|
||||||
|
&self.config.model
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_provider(&self) -> &AIProvider {
|
||||||
|
&self.config.provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience functions for creating common message types
|
||||||
|
impl ChatMessage {
|
||||||
|
pub fn user(content: impl Into<String>) -> Self {
|
||||||
|
ChatMessage {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: content.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assistant(content: impl Into<String>) -> Self {
|
||||||
|
ChatMessage {
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: content.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system(content: impl Into<String>) -> Self {
|
||||||
|
ChatMessage {
|
||||||
|
role: "system".to_string(),
|
||||||
|
content: content.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/chat.rs
45
src/chat.rs
@ -1,45 +0,0 @@
|
|||||||
// src/chat.rs
|
|
||||||
|
|
||||||
use seahorse::Context;
|
|
||||||
use std::process::Command;
|
|
||||||
//use std::env;
|
|
||||||
use crate::config::ConfigPaths;
|
|
||||||
|
|
||||||
pub fn ask_chat(c: &Context, question: &str) {
|
|
||||||
let config = ConfigPaths::new();
|
|
||||||
let base_dir = config.base_dir.join("mcp");
|
|
||||||
let script_path = base_dir.join("scripts/ask.py");
|
|
||||||
|
|
||||||
let python_path = if cfg!(target_os = "windows") {
|
|
||||||
base_dir.join(".venv/Scripts/python.exe")
|
|
||||||
} else {
|
|
||||||
base_dir.join(".venv/bin/python")
|
|
||||||
};
|
|
||||||
|
|
||||||
let ollama_host = c.string_flag("host").ok();
|
|
||||||
let ollama_model = c.string_flag("model").ok();
|
|
||||||
|
|
||||||
let mut command = Command::new(python_path);
|
|
||||||
command.arg(script_path).arg(question);
|
|
||||||
|
|
||||||
if let Some(host) = ollama_host {
|
|
||||||
command.env("OLLAMA_HOST", host);
|
|
||||||
}
|
|
||||||
if let Some(model) = ollama_model {
|
|
||||||
command.env("OLLAMA_MODEL", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = command
|
|
||||||
.output()
|
|
||||||
.expect("❌ MCPチャットスクリプトの実行に失敗しました");
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
println!("💬 {}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"❌ 実行エラー: {}\n{}",
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
String::from_utf8_lossy(&output.stdout),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
100
src/cli.rs
100
src/cli.rs
@ -1,100 +0,0 @@
|
|||||||
// src/cli.rs
|
|
||||||
use std::path::{Path};
|
|
||||||
use chrono::{Duration, Local};
|
|
||||||
use rusqlite::Connection;
|
|
||||||
|
|
||||||
use seahorse::{App, Command, Context};
|
|
||||||
|
|
||||||
use crate::utils::{load_config, save_config};
|
|
||||||
use crate::config::ConfigPaths;
|
|
||||||
use crate::agent::AIState;
|
|
||||||
use crate::commands::db::{save_cmd, export_cmd};
|
|
||||||
use crate::commands::scheduler::{scheduler_cmd};
|
|
||||||
use crate::commands::mcp::mcp_cmd;
|
|
||||||
|
|
||||||
pub fn cli_app() -> App {
|
|
||||||
let set_cmd = Command::new("set")
|
|
||||||
.usage("set [trust|intimacy|curiosity] [value]")
|
|
||||||
.action(|c: &Context| {
|
|
||||||
if c.args.len() != 2 {
|
|
||||||
eprintln!("Usage: set [trust|intimacy|curiosity] [value]");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let field = &c.args[0];
|
|
||||||
let value: f32 = c.args[1].parse().unwrap_or_else(|_| {
|
|
||||||
eprintln!("数値で入力してください");
|
|
||||||
std::process::exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ConfigPathsを使って設定ファイルのパスを取得
|
|
||||||
let config_paths = ConfigPaths::new();
|
|
||||||
let json_path = config_paths.data_file("json");
|
|
||||||
// まだ user.json がない場合、example.json をコピー
|
|
||||||
config_paths.ensure_file_exists("json", Path::new("example.json"));
|
|
||||||
let db_path = config_paths.data_file("db");
|
|
||||||
let mut ai = load_config(json_path.to_str().unwrap());
|
|
||||||
|
|
||||||
match field.as_str() {
|
|
||||||
"trust" => ai.relationship.trust = value,
|
|
||||||
"intimacy" => ai.relationship.intimacy = value,
|
|
||||||
"curiosity" => ai.relationship.curiosity = value,
|
|
||||||
_ => {
|
|
||||||
eprintln!("trust / intimacy / curiosity のいずれかを指定してください");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
save_config(json_path.to_str().unwrap(), &ai);
|
|
||||||
|
|
||||||
let conn = Connection::open(db_path.to_str().unwrap()).expect("DB接続失敗");
|
|
||||||
ai.save_to_db(&conn).expect("DB保存失敗");
|
|
||||||
|
|
||||||
println!("✅ {field} を {value} に更新しました");
|
|
||||||
});
|
|
||||||
|
|
||||||
let show_cmd = Command::new("show")
|
|
||||||
.usage("show")
|
|
||||||
.action(|_c: &Context| {
|
|
||||||
// ConfigPathsを使って設定ファイルのパスを取得
|
|
||||||
let config_paths = ConfigPaths::new();
|
|
||||||
let ai = load_config(config_paths.data_file("json").to_str().unwrap());
|
|
||||||
println!("🧠 現在のAI状態:\n{:#?}", ai);
|
|
||||||
});
|
|
||||||
|
|
||||||
let talk_cmd = Command::new("talk")
|
|
||||||
.usage("talk")
|
|
||||||
.action(|_c: &Context| {
|
|
||||||
let config_paths = ConfigPaths::new();
|
|
||||||
let ai = load_config(config_paths.data_file("json").to_str().unwrap());
|
|
||||||
|
|
||||||
let now = Local::now().naive_local();
|
|
||||||
let mut state = AIState {
|
|
||||||
relation_score: 80.0,
|
|
||||||
previous_score: 80.0,
|
|
||||||
decay_rate: ai.messaging.decay_rate,
|
|
||||||
sensitivity: ai.personality.strength,
|
|
||||||
message_threshold: 5.0,
|
|
||||||
last_message_time: now - Duration::days(4),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.update(now);
|
|
||||||
|
|
||||||
if state.should_talk() {
|
|
||||||
println!("💬 AI発話: {}", state.generate_message());
|
|
||||||
} else {
|
|
||||||
println!("🤫 今日は静かにしているみたい...");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
App::new("aigpt")
|
|
||||||
.version("0.1.0")
|
|
||||||
.description("AGE system CLI controller")
|
|
||||||
.author("syui")
|
|
||||||
.command(set_cmd)
|
|
||||||
.command(show_cmd)
|
|
||||||
.command(talk_cmd)
|
|
||||||
.command(save_cmd())
|
|
||||||
.command(export_cmd())
|
|
||||||
.command(scheduler_cmd())
|
|
||||||
.command(mcp_cmd())
|
|
||||||
}
|
|
74
src/cli/commands.rs
Normal file
74
src/cli/commands.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum TokenCommands {
|
||||||
|
/// Show Claude Code token usage summary and estimated costs
|
||||||
|
Summary {
|
||||||
|
/// Time period (today, week, month, all)
|
||||||
|
#[arg(long, default_value = "week")]
|
||||||
|
period: Option<String>,
|
||||||
|
/// Claude Code data directory path
|
||||||
|
#[arg(long)]
|
||||||
|
claude_dir: Option<PathBuf>,
|
||||||
|
/// Show detailed breakdown
|
||||||
|
#[arg(long)]
|
||||||
|
details: bool,
|
||||||
|
/// Output format (table, json)
|
||||||
|
#[arg(long, default_value = "table")]
|
||||||
|
format: Option<String>,
|
||||||
|
/// Cost calculation mode (auto, calculate, display)
|
||||||
|
#[arg(long, default_value = "auto")]
|
||||||
|
mode: Option<String>,
|
||||||
|
},
|
||||||
|
/// Show daily token usage breakdown
|
||||||
|
Daily {
|
||||||
|
/// Number of days to show
|
||||||
|
#[arg(long, default_value = "7")]
|
||||||
|
days: Option<u32>,
|
||||||
|
/// Claude Code data directory path
|
||||||
|
#[arg(long)]
|
||||||
|
claude_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Check Claude Code data availability and basic stats
|
||||||
|
Status {
|
||||||
|
/// Claude Code data directory path
|
||||||
|
#[arg(long)]
|
||||||
|
claude_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Analyze specific JSONL file (advanced)
|
||||||
|
Analyze {
|
||||||
|
/// Path to JSONL file to analyze
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
/// Generate beautiful token usage report using DuckDB (like the viral Claude Code usage visualization)
|
||||||
|
Report {
|
||||||
|
/// Number of days to include in report
|
||||||
|
#[arg(long, default_value = "7")]
|
||||||
|
days: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Show detailed cost breakdown by session (requires DuckDB)
|
||||||
|
Cost {
|
||||||
|
/// Month to analyze (YYYY-MM, 'today', 'current')
|
||||||
|
#[arg(long)]
|
||||||
|
month: Option<String>,
|
||||||
|
},
|
||||||
|
/// Show token usage breakdown by project
|
||||||
|
Projects {
|
||||||
|
/// Time period (today, week, month, all)
|
||||||
|
#[arg(long, default_value = "week")]
|
||||||
|
period: Option<String>,
|
||||||
|
/// Claude Code data directory path
|
||||||
|
#[arg(long)]
|
||||||
|
claude_dir: Option<PathBuf>,
|
||||||
|
/// Cost calculation mode (auto, calculate, display)
|
||||||
|
#[arg(long, default_value = "calculate")]
|
||||||
|
mode: Option<String>,
|
||||||
|
/// Show detailed breakdown
|
||||||
|
#[arg(long)]
|
||||||
|
details: bool,
|
||||||
|
/// Number of top projects to show
|
||||||
|
#[arg(long, default_value = "10")]
|
||||||
|
top: Option<u32>,
|
||||||
|
},
|
||||||
|
}
|
134
src/cli/mod.rs
Normal file
134
src/cli/mod.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::Result;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::mcp_server::MCPServer;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::transmission::TransmissionController;
|
||||||
|
use crate::scheduler::AIScheduler;
|
||||||
|
|
||||||
|
// Re-export from commands module
|
||||||
|
pub use commands::TokenCommands;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
pub async fn handle_server(port: Option<u16>, data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let port = port.unwrap_or(8080);
|
||||||
|
let config = Config::new(data_dir.clone())?;
|
||||||
|
|
||||||
|
let mut server = MCPServer::new(config, "mcp_user".to_string(), data_dir)?;
|
||||||
|
server.start_server(port).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_chat(
|
||||||
|
user_id: String,
|
||||||
|
message: String,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
model: Option<String>,
|
||||||
|
provider: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
let (response, relationship_delta) = if provider.is_some() || model.is_some() {
|
||||||
|
persona.process_ai_interaction(&user_id, &message, provider, model).await?
|
||||||
|
} else {
|
||||||
|
persona.process_interaction(&user_id, &message)?
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("AI Response: {}", response);
|
||||||
|
println!("Relationship Change: {:+.2}", relationship_delta);
|
||||||
|
|
||||||
|
if let Some(relationship) = persona.get_relationship(&user_id) {
|
||||||
|
println!("Relationship Status: {} (Score: {:.2})",
|
||||||
|
relationship.status, relationship.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_fortune(data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
let state = persona.get_current_state()?;
|
||||||
|
println!("🔮 Today's Fortune: {}", state.fortune_value);
|
||||||
|
println!("😊 Current Mood: {}", state.current_mood);
|
||||||
|
println!("✨ Breakthrough Status: {}",
|
||||||
|
if state.breakthrough_triggered { "Active" } else { "Inactive" });
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_relationships(data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
let relationships = persona.list_all_relationships();
|
||||||
|
|
||||||
|
if relationships.is_empty() {
|
||||||
|
println!("No relationships found.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📊 Relationships ({}):", relationships.len());
|
||||||
|
for (user_id, rel) in relationships {
|
||||||
|
println!(" {} - {} (Score: {:.2}, Interactions: {})",
|
||||||
|
user_id, rel.status, rel.score, rel.total_interactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_transmit(data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
let mut transmission_controller = TransmissionController::new(config)?;
|
||||||
|
|
||||||
|
let autonomous = transmission_controller.check_autonomous_transmissions(&mut persona).await?;
|
||||||
|
let breakthrough = transmission_controller.check_breakthrough_transmissions(&mut persona).await?;
|
||||||
|
let maintenance = transmission_controller.check_maintenance_transmissions(&mut persona).await?;
|
||||||
|
|
||||||
|
let total = autonomous.len() + breakthrough.len() + maintenance.len();
|
||||||
|
|
||||||
|
println!("📡 Transmission Check Complete:");
|
||||||
|
println!(" Autonomous: {}", autonomous.len());
|
||||||
|
println!(" Breakthrough: {}", breakthrough.len());
|
||||||
|
println!(" Maintenance: {}", maintenance.len());
|
||||||
|
println!(" Total: {}", total);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_maintenance(data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
let mut transmission_controller = TransmissionController::new(config)?;
|
||||||
|
|
||||||
|
persona.daily_maintenance()?;
|
||||||
|
let maintenance_transmissions = transmission_controller.check_maintenance_transmissions(&mut persona).await?;
|
||||||
|
|
||||||
|
let stats = persona.get_relationship_stats();
|
||||||
|
|
||||||
|
println!("🔧 Daily maintenance completed");
|
||||||
|
println!("📤 Maintenance transmissions sent: {}", maintenance_transmissions.len());
|
||||||
|
println!("📊 Relationship stats: {:?}", stats);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_schedule(data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
let mut transmission_controller = TransmissionController::new(config.clone())?;
|
||||||
|
let mut scheduler = AIScheduler::new(&config)?;
|
||||||
|
|
||||||
|
let executions = scheduler.run_scheduled_tasks(&mut persona, &mut transmission_controller).await?;
|
||||||
|
let stats = scheduler.get_scheduler_stats();
|
||||||
|
|
||||||
|
println!("⏰ Scheduler run completed");
|
||||||
|
println!("📋 Tasks executed: {}", executions.len());
|
||||||
|
println!("📊 Stats: {} total tasks, {} enabled, {:.2}% success rate",
|
||||||
|
stats.total_tasks, stats.enabled_tasks, stats.success_rate);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,44 +0,0 @@
|
|||||||
// src/commands/db.rs
|
|
||||||
use seahorse::{Command, Context};
|
|
||||||
use crate::utils::{load_config};
|
|
||||||
use crate::model::AiSystem;
|
|
||||||
use crate::config::ConfigPaths;
|
|
||||||
|
|
||||||
use rusqlite::Connection;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
pub fn save_cmd() -> Command {
|
|
||||||
Command::new("save")
|
|
||||||
.usage("save")
|
|
||||||
.action(|_c: &Context| {
|
|
||||||
let paths = ConfigPaths::new();
|
|
||||||
|
|
||||||
let json_path = paths.data_file("json");
|
|
||||||
let db_path = paths.data_file("db");
|
|
||||||
|
|
||||||
let ai = load_config(json_path.to_str().unwrap());
|
|
||||||
let conn = Connection::open(db_path).expect("DB接続失敗");
|
|
||||||
|
|
||||||
ai.save_to_db(&conn).expect("DB保存失敗");
|
|
||||||
println!("💾 DBに保存完了");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn export_cmd() -> Command {
|
|
||||||
Command::new("export")
|
|
||||||
.usage("export [output.json]")
|
|
||||||
.action(|c: &Context| {
|
|
||||||
let output_path = c.args.get(0).map(|s| s.as_str()).unwrap_or("output.json");
|
|
||||||
|
|
||||||
let paths = ConfigPaths::new();
|
|
||||||
let db_path = paths.data_file("db");
|
|
||||||
|
|
||||||
let conn = Connection::open(db_path).expect("DB接続失敗");
|
|
||||||
let ai = AiSystem::load_from_db(&conn).expect("DB読み込み失敗");
|
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&ai).expect("JSON変換失敗");
|
|
||||||
fs::write(output_path, json).expect("ファイル書き込み失敗");
|
|
||||||
|
|
||||||
println!("📤 JSONにエクスポート完了: {output_path}");
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
// src/commands/mcp.rs
|
|
||||||
|
|
||||||
use seahorse::{Command, Context, Flag, FlagType};
|
|
||||||
use crate::chat::ask_chat;
|
|
||||||
use crate::git::{git_init, git_status};
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{PathBuf};
|
|
||||||
use crate::config::ConfigPaths;
|
|
||||||
use std::process::Command as OtherCommand;
|
|
||||||
|
|
||||||
pub fn mcp_setup() {
|
|
||||||
let config = ConfigPaths::new();
|
|
||||||
let dest_dir = config.base_dir.join("mcp");
|
|
||||||
let repo_url = "https://github.com/microsoft/MCP.git";
|
|
||||||
println!("📁 MCP ディレクトリ: {}", dest_dir.display());
|
|
||||||
|
|
||||||
// 1. git clone(もしまだなければ)
|
|
||||||
if !dest_dir.exists() {
|
|
||||||
let status = OtherCommand::new("git")
|
|
||||||
.args(&["clone", repo_url, dest_dir.to_str().unwrap()])
|
|
||||||
.status()
|
|
||||||
.expect("git clone に失敗しました");
|
|
||||||
assert!(status.success(), "git clone 実行時にエラーが発生しました");
|
|
||||||
}
|
|
||||||
|
|
||||||
let asset_base = PathBuf::from("assets/mcp");
|
|
||||||
let files_to_copy = vec![
|
|
||||||
"cli.py",
|
|
||||||
"setup.py",
|
|
||||||
"scripts/ask.py",
|
|
||||||
"scripts/context_loader.py",
|
|
||||||
"scripts/prompt_template.py",
|
|
||||||
];
|
|
||||||
|
|
||||||
for rel_path in files_to_copy {
|
|
||||||
let src = asset_base.join(rel_path);
|
|
||||||
let dst = dest_dir.join(rel_path);
|
|
||||||
if let Some(parent) = dst.parent() {
|
|
||||||
let _ = fs::create_dir_all(parent);
|
|
||||||
}
|
|
||||||
if let Err(e) = fs::copy(&src, &dst) {
|
|
||||||
eprintln!("❌ コピー失敗: {} → {}: {}", src.display(), dst.display(), e);
|
|
||||||
} else {
|
|
||||||
println!("✅ コピー: {} → {}", src.display(), dst.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// venvの作成
|
|
||||||
let venv_path = dest_dir.join(".venv");
|
|
||||||
if !venv_path.exists() {
|
|
||||||
println!("🐍 仮想環境を作成しています...");
|
|
||||||
let output = OtherCommand::new("python3")
|
|
||||||
.args(&["-m", "venv", ".venv"])
|
|
||||||
.current_dir(&dest_dir)
|
|
||||||
.output()
|
|
||||||
.expect("venvの作成に失敗しました");
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `pip install -e .` を仮想環境で実行
|
|
||||||
let pip_path = if cfg!(target_os = "windows") {
|
|
||||||
dest_dir.join(".venv/Scripts/pip.exe").to_string_lossy().to_string()
|
|
||||||
} else {
|
|
||||||
dest_dir.join(".venv/bin/pip").to_string_lossy().to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("📦 必要なパッケージをインストールしています...");
|
|
||||||
let output = OtherCommand::new(&pip_path)
|
|
||||||
.arg("install")
|
|
||||||
.arg("openai")
|
|
||||||
.current_dir(&dest_dir)
|
|
||||||
.output()
|
|
||||||
.expect("pip install に失敗しました");
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
eprintln!(
|
|
||||||
"❌ pip エラー: {}\n{}",
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("📦 pip install -e . を実行します...");
|
|
||||||
let output = OtherCommand::new(&pip_path)
|
|
||||||
.arg("install")
|
|
||||||
.arg("-e")
|
|
||||||
.arg(".")
|
|
||||||
.current_dir(&dest_dir)
|
|
||||||
.output()
|
|
||||||
.expect("pip install に失敗しました");
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
println!("🎉 MCP セットアップが完了しました!");
|
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"❌ pip エラー: {}\n{}",
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chat_cmd() -> Command {
|
|
||||||
Command::new("chat")
|
|
||||||
.description("チャットで質問を送る")
|
|
||||||
.usage("mcp chat '質問内容' --host <OLLAMA_HOST> --model <OLLAMA_MODEL>")
|
|
||||||
.flag(Flag::new("host", FlagType::String).description("OLLAMAホストのURL"))
|
|
||||||
.flag(Flag::new("model", FlagType::String).description("OLLAMAモデル名"))
|
|
||||||
.action(|c: &Context| {
|
|
||||||
if let Some(question) = c.args.get(0) {
|
|
||||||
ask_chat(c, question);
|
|
||||||
} else {
|
|
||||||
eprintln!("❗ 質問が必要です: mcp chat 'こんにちは'");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_cmd() -> Command {
|
|
||||||
Command::new("init")
|
|
||||||
.description("Git 初期化")
|
|
||||||
.usage("mcp init")
|
|
||||||
.action(|_| {
|
|
||||||
git_init();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_cmd() -> Command {
|
|
||||||
Command::new("status")
|
|
||||||
.description("Git ステータス表示")
|
|
||||||
.usage("mcp status")
|
|
||||||
.action(|_| {
|
|
||||||
git_status();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_cmd() -> Command {
|
|
||||||
Command::new("setup")
|
|
||||||
.description("MCP の初期セットアップ")
|
|
||||||
.usage("mcp setup")
|
|
||||||
.action(|_| {
|
|
||||||
mcp_setup();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mcp_cmd() -> Command {
|
|
||||||
Command::new("mcp")
|
|
||||||
.description("MCP操作コマンド")
|
|
||||||
.usage("mcp <subcommand>")
|
|
||||||
.alias("m")
|
|
||||||
.command(chat_cmd())
|
|
||||||
.command(init_cmd())
|
|
||||||
.command(status_cmd())
|
|
||||||
.command(setup_cmd())
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
pub mod db;
|
|
||||||
pub mod scheduler;
|
|
||||||
pub mod mcp;
|
|
@ -1,29 +0,0 @@
|
|||||||
// src/commands/scheduler.rs
|
|
||||||
|
|
||||||
use seahorse::{Command, Context};
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use chrono::Local;
|
|
||||||
|
|
||||||
pub fn scheduler_cmd() -> Command {
|
|
||||||
Command::new("scheduler")
|
|
||||||
.usage("scheduler [interval_sec]")
|
|
||||||
.alias("s")
|
|
||||||
.action(|c: &Context| {
|
|
||||||
let interval = c.args.get(0)
|
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
|
||||||
.unwrap_or(60); // デフォルト: 60秒ごと
|
|
||||||
|
|
||||||
println!("⏳ スケジューラー開始({interval}秒ごと)...");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let now = Local::now();
|
|
||||||
println!("🔁 タスク実行中: {}", now.format("%Y-%m-%d %H:%M:%S"));
|
|
||||||
|
|
||||||
// ここで talk_cmd や save_cmd の内部処理を呼ぶ感じ
|
|
||||||
// たとえば load_config → AI更新 → print とか
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_secs(interval));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
273
src/config.rs
273
src/config.rs
@ -1,46 +1,251 @@
|
|||||||
// src/config.rs
|
use std::path::PathBuf;
|
||||||
use std::fs;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use serde::{Deserialize, Serialize};
|
||||||
use shellexpand;
|
use anyhow::{Result, Context};
|
||||||
|
|
||||||
pub struct ConfigPaths {
|
use crate::ai_provider::{AIConfig, AIProvider};
|
||||||
pub base_dir: PathBuf,
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub data_dir: PathBuf,
|
||||||
|
pub default_provider: String,
|
||||||
|
pub providers: HashMap<String, ProviderConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub atproto: Option<AtprotoConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mcp: Option<McpConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigPaths {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub fn new() -> Self {
|
pub struct ProviderConfig {
|
||||||
let app_name = env!("CARGO_PKG_NAME");
|
pub default_model: String,
|
||||||
let mut base_dir = shellexpand::tilde("~").to_string();
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
base_dir.push_str(&format!("/.config/{}/", app_name));
|
pub host: Option<String>,
|
||||||
let base_path = Path::new(&base_dir);
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
if !base_path.exists() {
|
pub api_key: Option<String>,
|
||||||
let _ = fs::create_dir_all(base_path);
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
}
|
pub system_prompt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
ConfigPaths {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
base_dir: base_path.to_path_buf(),
|
pub struct AtprotoConfig {
|
||||||
}
|
pub handle: Option<String>,
|
||||||
}
|
pub password: Option<String>,
|
||||||
|
pub host: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data_file(&self, file_name: &str) -> PathBuf {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
let file_path = match file_name {
|
pub struct McpConfig {
|
||||||
"db" => self.base_dir.join("user.db"),
|
#[serde(deserialize_with = "string_to_bool")]
|
||||||
"toml" => self.base_dir.join("user.toml"),
|
pub enabled: bool,
|
||||||
"json" => self.base_dir.join("user.json"),
|
#[serde(deserialize_with = "string_to_bool")]
|
||||||
_ => self.base_dir.join(format!(".{}", file_name)),
|
pub auto_detect: bool,
|
||||||
};
|
pub servers: HashMap<String, McpServerConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
file_path
|
fn string_to_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
match s.as_str() {
|
||||||
|
"true" => Ok(true),
|
||||||
|
"false" => Ok(false),
|
||||||
|
_ => Err(serde::de::Error::custom("expected 'true' or 'false'")),
|
||||||
}
|
}
|
||||||
/// 設定ファイルがなければ `example.json` をコピーする
|
}
|
||||||
pub fn ensure_file_exists(&self, file_name: &str, template_path: &Path) {
|
|
||||||
let target = self.data_file(file_name);
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
if !target.exists() {
|
pub struct McpServerConfig {
|
||||||
if let Err(e) = fs::copy(template_path, &target) {
|
pub base_url: String,
|
||||||
eprintln!("⚠️ 設定ファイルの初期化に失敗しました: {}", e);
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "string_to_f64")]
|
||||||
|
pub timeout: f64,
|
||||||
|
pub endpoints: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse::<f64>().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new(data_dir: Option<PathBuf>) -> Result<Self> {
|
||||||
|
let data_dir = data_dir.unwrap_or_else(|| {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join(".config")
|
||||||
|
.join("syui")
|
||||||
|
.join("ai")
|
||||||
|
.join("gpt")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.context("Failed to create data directory")?;
|
||||||
|
|
||||||
|
let config_path = data_dir.join("config.json");
|
||||||
|
|
||||||
|
// Try to load existing config
|
||||||
|
if config_path.exists() {
|
||||||
|
let config_str = std::fs::read_to_string(&config_path)
|
||||||
|
.context("Failed to read config.json")?;
|
||||||
|
|
||||||
|
// Check if file is empty
|
||||||
|
if config_str.trim().is_empty() {
|
||||||
|
eprintln!("Config file is empty, will recreate from source");
|
||||||
} else {
|
} else {
|
||||||
println!("📄 {} を {} にコピーしました", template_path.display(), target.display());
|
match serde_json::from_str::<Config>(&config_str) {
|
||||||
|
Ok(mut config) => {
|
||||||
|
config.data_dir = data_dir;
|
||||||
|
// Check for environment variables if API keys are empty
|
||||||
|
if let Some(openai_config) = config.providers.get_mut("openai") {
|
||||||
|
if openai_config.api_key.as_ref().map_or(true, |key| key.is_empty()) {
|
||||||
|
openai_config.api_key = std::env::var("OPENAI_API_KEY").ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse existing config.json: {}", e);
|
||||||
|
eprintln!("Will try to reload from source...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to migrate from JSON
|
||||||
|
// Try multiple locations for the JSON file
|
||||||
|
let possible_json_paths = vec![
|
||||||
|
PathBuf::from("../config.json"), // Relative to aigpt-rs directory
|
||||||
|
PathBuf::from("config.json"), // Current directory
|
||||||
|
PathBuf::from("gpt/config.json"), // From project root
|
||||||
|
PathBuf::from("/Users/syui/ai/ai/gpt/config.json"), // Absolute path
|
||||||
|
];
|
||||||
|
|
||||||
|
for json_path in possible_json_paths {
|
||||||
|
if json_path.exists() {
|
||||||
|
eprintln!("Found config.json at: {}", json_path.display());
|
||||||
|
eprintln!("Copying configuration...");
|
||||||
|
// Copy configuration file and parse it
|
||||||
|
std::fs::copy(&json_path, &config_path)
|
||||||
|
.context("Failed to copy config.json")?;
|
||||||
|
|
||||||
|
let config_str = std::fs::read_to_string(&config_path)
|
||||||
|
.context("Failed to read copied config.json")?;
|
||||||
|
|
||||||
|
println!("Config JSON content preview: {}", &config_str[..std::cmp::min(200, config_str.len())]);
|
||||||
|
|
||||||
|
let mut config: Config = serde_json::from_str(&config_str)
|
||||||
|
.context("Failed to parse config.json")?;
|
||||||
|
config.data_dir = data_dir;
|
||||||
|
// Check for environment variables if API keys are empty
|
||||||
|
if let Some(openai_config) = config.providers.get_mut("openai") {
|
||||||
|
if openai_config.api_key.as_ref().map_or(true, |key| key.is_empty()) {
|
||||||
|
openai_config.api_key = std::env::var("OPENAI_API_KEY").ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("Copy complete! Config saved to: {}", config_path.display());
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default config
|
||||||
|
let config = Self::default_config(data_dir);
|
||||||
|
|
||||||
|
// Save default config
|
||||||
|
let json_str = serde_json::to_string_pretty(&config)
|
||||||
|
.context("Failed to serialize default config")?;
|
||||||
|
std::fs::write(&config_path, json_str)
|
||||||
|
.context("Failed to write default config.json")?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
let config_path = self.data_dir.join("config.json");
|
||||||
|
let json_str = serde_json::to_string_pretty(self)
|
||||||
|
.context("Failed to serialize config")?;
|
||||||
|
std::fs::write(&config_path, json_str)
|
||||||
|
.context("Failed to write config.json")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_config(data_dir: PathBuf) -> Self {
|
||||||
|
let mut providers = HashMap::new();
|
||||||
|
|
||||||
|
providers.insert("ollama".to_string(), ProviderConfig {
|
||||||
|
default_model: "qwen2.5".to_string(),
|
||||||
|
host: Some("http://localhost:11434".to_string()),
|
||||||
|
api_key: None,
|
||||||
|
system_prompt: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
providers.insert("openai".to_string(), ProviderConfig {
|
||||||
|
default_model: "gpt-4o-mini".to_string(),
|
||||||
|
host: None,
|
||||||
|
api_key: std::env::var("OPENAI_API_KEY").ok(),
|
||||||
|
system_prompt: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Config {
|
||||||
|
data_dir,
|
||||||
|
default_provider: "ollama".to_string(),
|
||||||
|
providers,
|
||||||
|
atproto: None,
|
||||||
|
mcp: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_provider(&self, provider_name: &str) -> Option<&ProviderConfig> {
|
||||||
|
self.providers.get(provider_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ai_config(&self, provider: Option<String>, model: Option<String>) -> Result<AIConfig> {
|
||||||
|
let provider_name = provider.as_deref().unwrap_or(&self.default_provider);
|
||||||
|
let provider_config = self.get_provider(provider_name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Unknown provider: {}", provider_name))?;
|
||||||
|
|
||||||
|
let ai_provider: AIProvider = provider_name.parse()?;
|
||||||
|
let model_name = model.unwrap_or_else(|| provider_config.default_model.clone());
|
||||||
|
|
||||||
|
Ok(AIConfig {
|
||||||
|
provider: ai_provider,
|
||||||
|
model: model_name,
|
||||||
|
api_key: provider_config.api_key.clone(),
|
||||||
|
base_url: provider_config.host.clone(),
|
||||||
|
max_tokens: Some(2048),
|
||||||
|
temperature: Some(0.7),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn memory_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("memories.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relationships_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("relationships.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fortune_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("fortune.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transmission_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("transmissions.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduler_tasks_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("scheduler_tasks.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduler_history_file(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("scheduler_history.json")
|
||||||
}
|
}
|
||||||
}
|
}
|
205
src/conversation.rs
Normal file
205
src/conversation.rs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::*;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::http_client::ServiceDetector;
|
||||||
|
|
||||||
|
pub async fn handle_conversation(
|
||||||
|
user_id: String,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
model: Option<String>,
|
||||||
|
provider: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
println!("{}", "Starting conversation mode...".cyan());
|
||||||
|
println!("{}", "Type your message and press Enter to chat.".yellow());
|
||||||
|
println!("{}", "Available MCP commands: /memories, /search, /context, /relationship, /cards".yellow());
|
||||||
|
println!("{}", "Type 'exit', 'quit', or 'bye' to end conversation.".yellow());
|
||||||
|
println!("{}", "---".dimmed());
|
||||||
|
|
||||||
|
let mut conversation_history = Vec::new();
|
||||||
|
let service_detector = ServiceDetector::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Print prompt
|
||||||
|
print!("{} ", "You:".cyan().bold());
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
// Read user input
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
let input = input.trim();
|
||||||
|
|
||||||
|
// Check for exit commands
|
||||||
|
if matches!(input.to_lowercase().as_str(), "exit" | "quit" | "bye" | "") {
|
||||||
|
println!("{}", "Goodbye! 👋".green());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MCP commands
|
||||||
|
if input.starts_with('/') {
|
||||||
|
handle_mcp_command(input, &user_id, &service_detector).await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to conversation history
|
||||||
|
conversation_history.push(format!("User: {}", input));
|
||||||
|
|
||||||
|
// Get AI response
|
||||||
|
let (response, relationship_delta) = if provider.is_some() || model.is_some() {
|
||||||
|
persona.process_ai_interaction(&user_id, input, provider.clone(), model.clone()).await?
|
||||||
|
} else {
|
||||||
|
persona.process_interaction(&user_id, input)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add AI response to history
|
||||||
|
conversation_history.push(format!("AI: {}", response));
|
||||||
|
|
||||||
|
// Display response
|
||||||
|
println!("{} {}", "AI:".green().bold(), response);
|
||||||
|
|
||||||
|
// Show relationship change if significant
|
||||||
|
if relationship_delta.abs() >= 0.1 {
|
||||||
|
if relationship_delta > 0.0 {
|
||||||
|
println!("{}", format!(" └─ (+{:.2} relationship)", relationship_delta).green().dimmed());
|
||||||
|
} else {
|
||||||
|
println!("{}", format!(" └─ ({:.2} relationship)", relationship_delta).red().dimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(); // Add some spacing
|
||||||
|
|
||||||
|
// Keep conversation history manageable (last 20 exchanges)
|
||||||
|
if conversation_history.len() > 40 {
|
||||||
|
conversation_history.drain(0..20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_mcp_command(
|
||||||
|
command: &str,
|
||||||
|
user_id: &str,
|
||||||
|
service_detector: &ServiceDetector,
|
||||||
|
) -> Result<()> {
|
||||||
|
let parts: Vec<&str> = command[1..].split_whitespace().collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match parts[0] {
|
||||||
|
"memories" => {
|
||||||
|
println!("{}", "Retrieving memories...".yellow());
|
||||||
|
|
||||||
|
// Get contextual memories
|
||||||
|
if let Ok(memories) = service_detector.get_contextual_memories(user_id, 10).await {
|
||||||
|
if memories.is_empty() {
|
||||||
|
println!("No memories found for this conversation.");
|
||||||
|
} else {
|
||||||
|
println!("{}", format!("Found {} memories:", memories.len()).cyan());
|
||||||
|
for (i, memory) in memories.iter().enumerate() {
|
||||||
|
println!(" {}. {}", i + 1, memory.content);
|
||||||
|
println!(" {}", format!("({})", memory.created_at.format("%Y-%m-%d %H:%M")).dimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("{}", "Failed to retrieve memories.".red());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"search" => {
|
||||||
|
if parts.len() < 2 {
|
||||||
|
println!("{}", "Usage: /search <query>".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = parts[1..].join(" ");
|
||||||
|
println!("{}", format!("Searching for: '{}'", query).yellow());
|
||||||
|
|
||||||
|
if let Ok(results) = service_detector.search_memories(&query, 5).await {
|
||||||
|
if results.is_empty() {
|
||||||
|
println!("No relevant memories found.");
|
||||||
|
} else {
|
||||||
|
println!("{}", format!("Found {} relevant memories:", results.len()).cyan());
|
||||||
|
for (i, memory) in results.iter().enumerate() {
|
||||||
|
println!(" {}. {}", i + 1, memory.content);
|
||||||
|
println!(" {}", format!("({})", memory.created_at.format("%Y-%m-%d %H:%M")).dimmed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("{}", "Search failed.".red());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"context" => {
|
||||||
|
println!("{}", "Creating context summary...".yellow());
|
||||||
|
|
||||||
|
if let Ok(summary) = service_detector.create_summary(user_id).await {
|
||||||
|
println!("{}", "Context Summary:".cyan().bold());
|
||||||
|
println!("{}", summary);
|
||||||
|
} else {
|
||||||
|
println!("{}", "Failed to create context summary.".red());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"relationship" => {
|
||||||
|
println!("{}", "Checking relationship status...".yellow());
|
||||||
|
|
||||||
|
// This would need to be implemented in the service client
|
||||||
|
println!("{}", "Relationship status: Active".cyan());
|
||||||
|
println!("Score: 85.5 / 100");
|
||||||
|
println!("Transmission: ✓ Enabled");
|
||||||
|
},
|
||||||
|
|
||||||
|
"cards" => {
|
||||||
|
println!("{}", "Checking card collection...".yellow());
|
||||||
|
|
||||||
|
// Try to connect to ai.card service
|
||||||
|
if let Ok(stats) = service_detector.get_card_stats().await {
|
||||||
|
println!("{}", "Card Collection:".cyan().bold());
|
||||||
|
println!(" Total Cards: {}", stats.get("total").unwrap_or(&serde_json::Value::Number(0.into())));
|
||||||
|
println!(" Unique Cards: {}", stats.get("unique").unwrap_or(&serde_json::Value::Number(0.into())));
|
||||||
|
|
||||||
|
// Offer to draw a card
|
||||||
|
println!("\n{}", "Would you like to draw a card? (y/n)".yellow());
|
||||||
|
let mut response = String::new();
|
||||||
|
io::stdin().read_line(&mut response)?;
|
||||||
|
if response.trim().to_lowercase() == "y" {
|
||||||
|
println!("{}", "Drawing card...".cyan());
|
||||||
|
if let Ok(card) = service_detector.draw_card(user_id, false).await {
|
||||||
|
println!("{}", "🎴 Card drawn!".green().bold());
|
||||||
|
println!("Name: {}", card.get("name").unwrap_or(&serde_json::Value::String("Unknown".to_string())));
|
||||||
|
println!("Rarity: {}", card.get("rarity").unwrap_or(&serde_json::Value::String("Unknown".to_string())));
|
||||||
|
} else {
|
||||||
|
println!("{}", "Failed to draw card. ai.card service might not be running.".red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("{}", "ai.card service not available.".red());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"help" | "h" => {
|
||||||
|
println!("{}", "Available MCP Commands:".cyan().bold());
|
||||||
|
println!(" {:<15} - Show recent memories for this conversation", "/memories".yellow());
|
||||||
|
println!(" {:<15} - Search memories by keyword", "/search <query>".yellow());
|
||||||
|
println!(" {:<15} - Create a context summary", "/context".yellow());
|
||||||
|
println!(" {:<15} - Show relationship status", "/relationship".yellow());
|
||||||
|
println!(" {:<15} - Show card collection and draw cards", "/cards".yellow());
|
||||||
|
println!(" {:<15} - Show this help message", "/help".yellow());
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
println!("{}", format!("Unknown command: /{}. Type '/help' for available commands.", parts[0]).red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(); // Add spacing after MCP command output
|
||||||
|
Ok(())
|
||||||
|
}
|
789
src/docs.rs
Normal file
789
src/docs.rs
Normal file
@ -0,0 +1,789 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use colored::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::ai_provider::{AIProviderClient, AIConfig, AIProvider};
|
||||||
|
|
||||||
|
pub async fn handle_docs(
|
||||||
|
action: String,
|
||||||
|
project: Option<String>,
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
ai_integration: bool,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut docs_manager = DocsManager::new(config);
|
||||||
|
|
||||||
|
match action.as_str() {
|
||||||
|
"generate" => {
|
||||||
|
if let Some(project_name) = project {
|
||||||
|
docs_manager.generate_project_docs(&project_name, output, ai_integration).await?;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Project name is required for generate action"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sync" => {
|
||||||
|
if let Some(project_name) = project {
|
||||||
|
docs_manager.sync_project_docs(&project_name).await?;
|
||||||
|
} else {
|
||||||
|
docs_manager.sync_all_docs().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"list" => {
|
||||||
|
docs_manager.list_projects().await?;
|
||||||
|
}
|
||||||
|
"status" => {
|
||||||
|
docs_manager.show_docs_status().await?;
|
||||||
|
}
|
||||||
|
"session-end" => {
|
||||||
|
docs_manager.session_end_processing().await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!("Unknown docs action: {}", action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProjectInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub project_type: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: String,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
pub dependencies: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
ProjectInfo {
|
||||||
|
name: String::new(),
|
||||||
|
project_type: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
features: Vec::new(),
|
||||||
|
dependencies: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DocsManager {
|
||||||
|
config: Config,
|
||||||
|
ai_root: PathBuf,
|
||||||
|
projects: HashMap<String, ProjectInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocsManager {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let ai_root = dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join("ai")
|
||||||
|
.join("ai");
|
||||||
|
|
||||||
|
DocsManager {
|
||||||
|
config,
|
||||||
|
ai_root,
|
||||||
|
projects: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_project_docs(&mut self, project: &str, output: Option<PathBuf>, ai_integration: bool) -> Result<()> {
|
||||||
|
println!("{}", format!("📝 Generating documentation for project '{}'", project).cyan().bold());
|
||||||
|
|
||||||
|
// Load project information
|
||||||
|
let project_info = self.load_project_info(project)?;
|
||||||
|
|
||||||
|
// Generate documentation content
|
||||||
|
let mut content = self.generate_base_documentation(&project_info)?;
|
||||||
|
|
||||||
|
// AI enhancement if requested
|
||||||
|
if ai_integration {
|
||||||
|
println!("{}", "🤖 Enhancing documentation with AI...".blue());
|
||||||
|
if let Ok(enhanced_content) = self.enhance_with_ai(project, &content).await {
|
||||||
|
content = enhanced_content;
|
||||||
|
} else {
|
||||||
|
println!("{}", "Warning: AI enhancement failed, using base documentation".yellow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine output path
|
||||||
|
let output_path = if let Some(path) = output {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
self.ai_root.join(project).join("claude.md")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write documentation
|
||||||
|
std::fs::write(&output_path, content)
|
||||||
|
.with_context(|| format!("Failed to write documentation to: {}", output_path.display()))?;
|
||||||
|
|
||||||
|
println!("{}", format!("✅ Documentation generated: {}", output_path.display()).green().bold());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_project_docs(&self, project: &str) -> Result<()> {
|
||||||
|
println!("{}", format!("🔄 Syncing documentation for project '{}'", project).cyan().bold());
|
||||||
|
|
||||||
|
let claude_dir = self.ai_root.join("claude");
|
||||||
|
let project_dir = self.ai_root.join(project);
|
||||||
|
|
||||||
|
// Check if claude directory exists
|
||||||
|
if !claude_dir.exists() {
|
||||||
|
return Err(anyhow::anyhow!("Claude directory not found: {}", claude_dir.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy relevant files
|
||||||
|
let files_to_sync = vec!["README.md", "claude.md", "DEVELOPMENT.md"];
|
||||||
|
|
||||||
|
for file in files_to_sync {
|
||||||
|
let src = claude_dir.join("projects").join(format!("{}.md", project));
|
||||||
|
let dst = project_dir.join(file);
|
||||||
|
|
||||||
|
if src.exists() {
|
||||||
|
if let Some(parent) = dst.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::copy(&src, &dst)?;
|
||||||
|
println!(" ✓ Synced: {}", file.green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "✅ Documentation sync completed".green().bold());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_all_docs(&self) -> Result<()> {
|
||||||
|
println!("{}", "🔄 Syncing documentation for all projects...".cyan().bold());
|
||||||
|
|
||||||
|
// Find all project directories
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
|
||||||
|
for project in projects {
|
||||||
|
println!("\n{}", format!("Syncing: {}", project).blue());
|
||||||
|
if let Err(e) = self.sync_project_docs(&project).await {
|
||||||
|
println!("{}: {}", "Warning".yellow(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ai.wiki content after all project syncs
|
||||||
|
println!("\n{}", "📝 Updating ai.wiki...".blue());
|
||||||
|
if let Err(e) = self.update_ai_wiki().await {
|
||||||
|
println!("{}: Failed to update ai.wiki: {}", "Warning".yellow(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update repository wiki (Gitea wiki) as well
|
||||||
|
println!("\n{}", "📝 Updating repository wiki...".blue());
|
||||||
|
if let Err(e) = self.update_repository_wiki().await {
|
||||||
|
println!("{}: Failed to update repository wiki: {}", "Warning".yellow(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n{}", "✅ All projects synced".green().bold());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_projects(&mut self) -> Result<()> {
|
||||||
|
println!("{}", "📋 Available Projects".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
|
||||||
|
if projects.is_empty() {
|
||||||
|
println!("{}", "No projects found".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load project information
|
||||||
|
for project in &projects {
|
||||||
|
if let Ok(info) = self.load_project_info(project) {
|
||||||
|
self.projects.insert(project.clone(), info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display projects in a table format
|
||||||
|
println!("{:<20} {:<15} {:<15} {}",
|
||||||
|
"Project".cyan().bold(),
|
||||||
|
"Type".cyan().bold(),
|
||||||
|
"Status".cyan().bold(),
|
||||||
|
"Description".cyan().bold());
|
||||||
|
println!("{}", "-".repeat(80));
|
||||||
|
|
||||||
|
let project_count = projects.len();
|
||||||
|
for project in &projects {
|
||||||
|
let info = self.projects.get(project).cloned().unwrap_or_default();
|
||||||
|
let status_color = match info.status.as_str() {
|
||||||
|
"active" => info.status.green(),
|
||||||
|
"development" => info.status.yellow(),
|
||||||
|
"deprecated" => info.status.red(),
|
||||||
|
_ => info.status.normal(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:<20} {:<15} {:<15} {}",
|
||||||
|
project.blue(),
|
||||||
|
info.project_type,
|
||||||
|
status_color,
|
||||||
|
info.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("Total projects: {}", project_count.to_string().cyan());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_docs_status(&self) -> Result<()> {
|
||||||
|
println!("{}", "📊 Documentation Status".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
let mut total_files = 0;
|
||||||
|
let mut total_lines = 0;
|
||||||
|
|
||||||
|
for project in projects {
|
||||||
|
let project_dir = self.ai_root.join(&project);
|
||||||
|
let claude_md = project_dir.join("claude.md");
|
||||||
|
|
||||||
|
if claude_md.exists() {
|
||||||
|
let content = std::fs::read_to_string(&claude_md)?;
|
||||||
|
let lines = content.lines().count();
|
||||||
|
let size = content.len();
|
||||||
|
|
||||||
|
println!("{}: {} lines, {} bytes",
|
||||||
|
project.blue(),
|
||||||
|
lines.to_string().yellow(),
|
||||||
|
size.to_string().yellow());
|
||||||
|
|
||||||
|
total_files += 1;
|
||||||
|
total_lines += lines;
|
||||||
|
} else {
|
||||||
|
println!("{}: {}", project.blue(), "No documentation".red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("Summary: {} files, {} total lines",
|
||||||
|
total_files.to_string().cyan(),
|
||||||
|
total_lines.to_string().cyan());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_projects(&self) -> Result<Vec<String>> {
|
||||||
|
let mut projects = Vec::new();
|
||||||
|
|
||||||
|
// Known project directories
|
||||||
|
let known_projects = vec![
|
||||||
|
"gpt", "card", "bot", "shell", "os", "game", "moji", "verse"
|
||||||
|
];
|
||||||
|
|
||||||
|
for project in known_projects {
|
||||||
|
let project_dir = self.ai_root.join(project);
|
||||||
|
if project_dir.exists() && project_dir.is_dir() {
|
||||||
|
projects.push(project.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also scan for additional directories with ai.json
|
||||||
|
if self.ai_root.exists() {
|
||||||
|
for entry in std::fs::read_dir(&self.ai_root)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
let ai_json = path.join("ai.json");
|
||||||
|
if ai_json.exists() {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if !projects.contains(&name.to_string()) {
|
||||||
|
projects.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projects.sort();
|
||||||
|
Ok(projects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_project_info(&self, project: &str) -> Result<ProjectInfo> {
|
||||||
|
let ai_json_path = self.ai_root.join(project).join("ai.json");
|
||||||
|
|
||||||
|
if ai_json_path.exists() {
|
||||||
|
let content = std::fs::read_to_string(&ai_json_path)?;
|
||||||
|
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
|
let mut info = ProjectInfo::default();
|
||||||
|
info.name = project.to_string();
|
||||||
|
|
||||||
|
if let Some(project_data) = json_data.get(project) {
|
||||||
|
if let Some(type_str) = project_data.get("type").and_then(|v| v.as_str()) {
|
||||||
|
info.project_type = type_str.to_string();
|
||||||
|
}
|
||||||
|
if let Some(desc) = project_data.get("description").and_then(|v| v.as_str()) {
|
||||||
|
info.description = desc.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default project info based on known projects
|
||||||
|
let mut info = ProjectInfo::default();
|
||||||
|
info.name = project.to_string();
|
||||||
|
|
||||||
|
match project {
|
||||||
|
"gpt" => {
|
||||||
|
info.project_type = "AI".to_string();
|
||||||
|
info.description = "Autonomous transmission AI with unique personality".to_string();
|
||||||
|
}
|
||||||
|
"card" => {
|
||||||
|
info.project_type = "Game".to_string();
|
||||||
|
info.description = "Card game system with atproto integration".to_string();
|
||||||
|
}
|
||||||
|
"bot" => {
|
||||||
|
info.project_type = "Bot".to_string();
|
||||||
|
info.description = "Distributed SNS bot for AI ecosystem".to_string();
|
||||||
|
}
|
||||||
|
"shell" => {
|
||||||
|
info.project_type = "Tool".to_string();
|
||||||
|
info.description = "AI-powered shell interface".to_string();
|
||||||
|
}
|
||||||
|
"os" => {
|
||||||
|
info.project_type = "OS".to_string();
|
||||||
|
info.description = "Game-oriented operating system".to_string();
|
||||||
|
}
|
||||||
|
"verse" => {
|
||||||
|
info.project_type = "Metaverse".to_string();
|
||||||
|
info.description = "Reality-reflecting 3D world system".to_string();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
info.project_type = "Unknown".to_string();
|
||||||
|
info.description = format!("AI ecosystem project: {}", project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_base_documentation(&self, project_info: &ProjectInfo) -> Result<String> {
|
||||||
|
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
content.push_str(&format!("# {}\n\n", project_info.name));
|
||||||
|
content.push_str(&format!("## Overview\n\n"));
|
||||||
|
content.push_str(&format!("**Type**: {}\n\n", project_info.project_type));
|
||||||
|
content.push_str(&format!("**Description**: {}\n\n", project_info.description));
|
||||||
|
content.push_str(&format!("**Status**: {}\n\n", project_info.status));
|
||||||
|
|
||||||
|
if !project_info.features.is_empty() {
|
||||||
|
content.push_str("## Features\n\n");
|
||||||
|
for feature in &project_info.features {
|
||||||
|
content.push_str(&format!("- {}\n", feature));
|
||||||
|
}
|
||||||
|
content.push_str("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push_str("## Architecture\n\n");
|
||||||
|
content.push_str("This project is part of the ai ecosystem, following the core principles:\n\n");
|
||||||
|
content.push_str("- **Existence Theory**: Based on the exploration of the smallest units (ai/existon)\n");
|
||||||
|
content.push_str("- **Uniqueness Principle**: Ensuring 1:1 mapping between reality and digital existence\n");
|
||||||
|
content.push_str("- **Reality Reflection**: Creating circular influence between reality and game\n\n");
|
||||||
|
|
||||||
|
content.push_str("## Development\n\n");
|
||||||
|
content.push_str("### Getting Started\n\n");
|
||||||
|
content.push_str("```bash\n");
|
||||||
|
content.push_str(&format!("# Clone the repository\n"));
|
||||||
|
content.push_str(&format!("git clone https://git.syui.ai/ai/{}\n", project_info.name));
|
||||||
|
content.push_str(&format!("cd {}\n", project_info.name));
|
||||||
|
content.push_str("```\n\n");
|
||||||
|
|
||||||
|
content.push_str("### Configuration\n\n");
|
||||||
|
content.push_str(&format!("Configuration files are stored in `~/.config/syui/ai/{}/`\n\n", project_info.name));
|
||||||
|
|
||||||
|
content.push_str("## Integration\n\n");
|
||||||
|
content.push_str("This project integrates with other ai ecosystem components:\n\n");
|
||||||
|
if !project_info.dependencies.is_empty() {
|
||||||
|
for dep in &project_info.dependencies {
|
||||||
|
content.push_str(&format!("- **{}**: Core dependency\n", dep));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.push_str("- **ai.gpt**: Core AI personality system\n");
|
||||||
|
content.push_str("- **atproto**: Distributed identity and data\n");
|
||||||
|
}
|
||||||
|
content.push_str("\n");
|
||||||
|
|
||||||
|
content.push_str("---\n\n");
|
||||||
|
content.push_str(&format!("*Generated: {}*\n", timestamp));
|
||||||
|
content.push_str("*🤖 Generated with [Claude Code](https://claude.ai/code)*\n");
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enhance_with_ai(&self, project: &str, base_content: &str) -> Result<String> {
|
||||||
|
// Create AI provider
|
||||||
|
let ai_config = AIConfig {
|
||||||
|
provider: AIProvider::Ollama,
|
||||||
|
model: "llama2".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
max_tokens: Some(2000),
|
||||||
|
temperature: Some(0.7),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ai_provider = AIProviderClient::new(ai_config);
|
||||||
|
let mut persona = Persona::new(&self.config)?;
|
||||||
|
|
||||||
|
let enhancement_prompt = format!(
|
||||||
|
"As an AI documentation expert, enhance the following documentation for project '{}'.
|
||||||
|
|
||||||
|
Current documentation:
|
||||||
|
{}
|
||||||
|
|
||||||
|
Please provide enhanced content that includes:
|
||||||
|
1. More detailed project description
|
||||||
|
2. Key features and capabilities
|
||||||
|
3. Usage examples
|
||||||
|
4. Integration points with other AI ecosystem projects
|
||||||
|
5. Development workflow recommendations
|
||||||
|
|
||||||
|
Keep the same structure but expand and improve the content.",
|
||||||
|
project, base_content
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to get AI response
|
||||||
|
let (response, _) = persona.process_ai_interaction(
|
||||||
|
"docs_system",
|
||||||
|
&enhancement_prompt,
|
||||||
|
Some("ollama".to_string()),
|
||||||
|
Some("llama2".to_string())
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// If AI response is substantial, use it; otherwise fall back to base content
|
||||||
|
if response.len() > base_content.len() / 2 {
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Ok(base_content.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// セッション終了時の処理(ドキュメント記録・同期)
|
||||||
|
pub async fn session_end_processing(&mut self) -> Result<()> {
|
||||||
|
println!("{}", "🔄 Session end processing started...".cyan());
|
||||||
|
|
||||||
|
// 1. 現在のプロジェクト状況を記録
|
||||||
|
println!("📊 Recording current project status...");
|
||||||
|
self.record_session_summary().await?;
|
||||||
|
|
||||||
|
// 2. 全プロジェクトのドキュメント同期
|
||||||
|
println!("🔄 Syncing all project documentation...");
|
||||||
|
self.sync_all_docs().await?;
|
||||||
|
|
||||||
|
// 3. READMEの自動更新
|
||||||
|
println!("📝 Updating project README files...");
|
||||||
|
self.update_project_readmes().await?;
|
||||||
|
|
||||||
|
// 4. メタデータの更新
|
||||||
|
println!("🏷️ Updating project metadata...");
|
||||||
|
self.update_project_metadata().await?;
|
||||||
|
|
||||||
|
println!("{}", "✅ Session end processing completed!".green());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// セッション概要を記録
|
||||||
|
async fn record_session_summary(&self) -> Result<()> {
|
||||||
|
let session_log_path = self.ai_root.join("session_logs");
|
||||||
|
std::fs::create_dir_all(&session_log_path)?;
|
||||||
|
|
||||||
|
let timestamp = Utc::now().format("%Y-%m-%d_%H-%M-%S");
|
||||||
|
let log_file = session_log_path.join(format!("session_{}.md", timestamp));
|
||||||
|
|
||||||
|
let summary = format!(
|
||||||
|
"# Session Summary - {}\n\n\
|
||||||
|
## Timestamp\n{}\n\n\
|
||||||
|
## Projects Status\n{}\n\n\
|
||||||
|
## Next Actions\n- Documentation sync completed\n- README files updated\n- Metadata refreshed\n\n\
|
||||||
|
---\n*Generated by aigpt session-end processing*\n",
|
||||||
|
timestamp,
|
||||||
|
Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
|
||||||
|
self.generate_projects_status().await.unwrap_or_else(|_| "Status unavailable".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(log_file, summary)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// プロジェクト状況を生成
|
||||||
|
async fn generate_projects_status(&self) -> Result<String> {
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
let mut status = String::new();
|
||||||
|
|
||||||
|
for project in projects {
|
||||||
|
let claude_md = self.ai_root.join(&project).join("claude.md");
|
||||||
|
let readme_md = self.ai_root.join(&project).join("README.md");
|
||||||
|
|
||||||
|
status.push_str(&format!("- **{}**: ", project));
|
||||||
|
if claude_md.exists() {
|
||||||
|
status.push_str("claude.md ✅ ");
|
||||||
|
} else {
|
||||||
|
status.push_str("claude.md ❌ ");
|
||||||
|
}
|
||||||
|
if readme_md.exists() {
|
||||||
|
status.push_str("README.md ✅");
|
||||||
|
} else {
|
||||||
|
status.push_str("README.md ❌");
|
||||||
|
}
|
||||||
|
status.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ai.wikiの更新処理
|
||||||
|
async fn update_ai_wiki(&self) -> Result<()> {
|
||||||
|
let ai_wiki_path = self.ai_root.join("ai.wiki");
|
||||||
|
|
||||||
|
// ai.wikiディレクトリが存在することを確認
|
||||||
|
if !ai_wiki_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!("ai.wiki directory not found at {:?}", ai_wiki_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home.mdの生成
|
||||||
|
let home_content = self.generate_wiki_home_content().await?;
|
||||||
|
let home_path = ai_wiki_path.join("Home.md");
|
||||||
|
std::fs::write(&home_path, &home_content)?;
|
||||||
|
println!(" ✓ Updated: {}", "Home.md".green());
|
||||||
|
|
||||||
|
// title.mdの生成 (Gitea wiki特別ページ用)
|
||||||
|
let title_path = ai_wiki_path.join("title.md");
|
||||||
|
std::fs::write(&title_path, &home_content)?;
|
||||||
|
println!(" ✓ Updated: {}", "title.md".green());
|
||||||
|
|
||||||
|
// プロジェクト個別ディレクトリの更新
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
for project in projects {
|
||||||
|
let project_dir = ai_wiki_path.join(&project);
|
||||||
|
std::fs::create_dir_all(&project_dir)?;
|
||||||
|
|
||||||
|
let project_content = self.generate_auto_project_content(&project).await?;
|
||||||
|
let project_file = project_dir.join(format!("{}.md", project));
|
||||||
|
std::fs::write(&project_file, project_content)?;
|
||||||
|
println!(" ✓ Updated: {}", format!("{}/{}.md", project, project).green());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "✅ ai.wiki updated successfully".green().bold());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ai.wiki/Home.mdのコンテンツ生成
|
||||||
|
async fn generate_wiki_home_content(&self) -> Result<String> {
|
||||||
|
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S");
|
||||||
|
let mut content = String::new();
|
||||||
|
|
||||||
|
content.push_str("# AI Ecosystem Wiki\n\n");
|
||||||
|
content.push_str("AI生態系プロジェクトの概要とドキュメント集約ページです。\n\n");
|
||||||
|
content.push_str("## プロジェクト一覧\n\n");
|
||||||
|
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
let mut project_sections = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// プロジェクトをカテゴリ別に分類
|
||||||
|
for project in &projects {
|
||||||
|
let info = self.load_project_info(project).unwrap_or_default();
|
||||||
|
let category = match project.as_str() {
|
||||||
|
"ai" => "🧠 AI・知能システム",
|
||||||
|
"gpt" => "🤖 自律・対話システム",
|
||||||
|
"os" => "💻 システム・基盤",
|
||||||
|
"game" => "📁 device",
|
||||||
|
"card" => "🎮 ゲーム・エンターテイメント",
|
||||||
|
"bot" | "moji" | "api" | "log" => "📁 その他",
|
||||||
|
"verse" => "📁 metaverse",
|
||||||
|
"shell" => "⚡ ツール・ユーティリティ",
|
||||||
|
_ => "📁 その他",
|
||||||
|
};
|
||||||
|
|
||||||
|
project_sections.entry(category).or_insert_with(Vec::new).push((project.clone(), info));
|
||||||
|
}
|
||||||
|
|
||||||
|
// カテゴリ別にプロジェクトを出力
|
||||||
|
let mut categories: Vec<_> = project_sections.keys().collect();
|
||||||
|
categories.sort();
|
||||||
|
|
||||||
|
for category in categories {
|
||||||
|
content.push_str(&format!("### {}\n\n", category));
|
||||||
|
|
||||||
|
if let Some(projects_in_category) = project_sections.get(category) {
|
||||||
|
for (project, info) in projects_in_category {
|
||||||
|
content.push_str(&format!("#### [{}]({}.md)\n", project, project));
|
||||||
|
|
||||||
|
if !info.description.is_empty() {
|
||||||
|
content.push_str(&format!("- **名前**: ai.{} - **パッケージ**: ai{} - **タイプ**: {} - **役割**: {}\n\n",
|
||||||
|
project, project, info.project_type, info.description));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push_str(&format!("**Status**: {} \n", info.status));
|
||||||
|
let branch = self.get_project_branch(project);
|
||||||
|
content.push_str(&format!("**Links**: [Repo](https://git.syui.ai/ai/{}) | [Docs](https://git.syui.ai/ai/{}/src/branch/{}/claude.md)\n\n", project, project, branch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push_str("---\n\n");
|
||||||
|
content.push_str("## ディレクトリ構成\n\n");
|
||||||
|
content.push_str("- `{project}/` - プロジェクト個別ドキュメント\n");
|
||||||
|
content.push_str("- `claude/` - Claude Code作業記録\n");
|
||||||
|
content.push_str("- `manual/` - 手動作成ドキュメント\n\n");
|
||||||
|
content.push_str("---\n\n");
|
||||||
|
content.push_str("*このページは ai.json と claude/projects/ から自動生成されました* \n");
|
||||||
|
content.push_str(&format!("*最終更新: {}*\n", timestamp));
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// プロジェクト個別ファイルのコンテンツ生成
|
||||||
|
async fn generate_auto_project_content(&self, project: &str) -> Result<String> {
|
||||||
|
let info = self.load_project_info(project).unwrap_or_default();
|
||||||
|
let mut content = String::new();
|
||||||
|
|
||||||
|
content.push_str(&format!("# {}\n\n", project));
|
||||||
|
content.push_str("## 概要\n");
|
||||||
|
content.push_str(&format!("- **名前**: ai.{} - **パッケージ**: ai{} - **タイプ**: {} - **役割**: {}\n\n",
|
||||||
|
project, project, info.project_type, info.description));
|
||||||
|
|
||||||
|
content.push_str("## プロジェクト情報\n");
|
||||||
|
content.push_str(&format!("- **タイプ**: {}\n", info.project_type));
|
||||||
|
content.push_str(&format!("- **説明**: {}\n", info.description));
|
||||||
|
content.push_str(&format!("- **ステータス**: {}\n", info.status));
|
||||||
|
let branch = self.get_project_branch(project);
|
||||||
|
content.push_str(&format!("- **ブランチ**: {}\n", branch));
|
||||||
|
content.push_str("- **最終更新**: Unknown\n\n");
|
||||||
|
|
||||||
|
// プロジェクト固有の機能情報を追加
|
||||||
|
if !info.features.is_empty() {
|
||||||
|
content.push_str("## 主な機能・特徴\n");
|
||||||
|
for feature in &info.features {
|
||||||
|
content.push_str(&format!("- {}\n", feature));
|
||||||
|
}
|
||||||
|
content.push_str("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push_str("## リンク\n");
|
||||||
|
content.push_str(&format!("- **Repository**: https://git.syui.ai/ai/{}\n", project));
|
||||||
|
content.push_str(&format!("- **Project Documentation**: [claude/projects/{}.md](https://git.syui.ai/ai/ai/src/branch/main/claude/projects/{}.md)\n", project, project));
|
||||||
|
let branch = self.get_project_branch(project);
|
||||||
|
content.push_str(&format!("- **Generated Documentation**: [{}/claude.md](https://git.syui.ai/ai/{}/src/branch/{}/claude.md)\n\n", project, project, branch));
|
||||||
|
|
||||||
|
content.push_str("---\n");
|
||||||
|
content.push_str(&format!("*このページは claude/projects/{}.md から自動生成されました*\n", project));
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// リポジトリwiki (Gitea wiki) の更新処理
|
||||||
|
async fn update_repository_wiki(&self) -> Result<()> {
|
||||||
|
println!(" ℹ️ Repository wiki is now unified with ai.wiki");
|
||||||
|
println!(" ℹ️ ai.wiki serves as the source of truth (git@git.syui.ai:ai/ai.wiki.git)");
|
||||||
|
println!(" ℹ️ Special pages generated: Home.md, title.md for Gitea wiki compatibility");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// プロジェクトREADMEファイルの更新
|
||||||
|
async fn update_project_readmes(&self) -> Result<()> {
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
|
||||||
|
for project in projects {
|
||||||
|
let readme_path = self.ai_root.join(&project).join("README.md");
|
||||||
|
let claude_md_path = self.ai_root.join(&project).join("claude.md");
|
||||||
|
|
||||||
|
// claude.mdが存在する場合、READMEに同期
|
||||||
|
if claude_md_path.exists() {
|
||||||
|
let claude_content = std::fs::read_to_string(&claude_md_path)?;
|
||||||
|
|
||||||
|
// READMEが存在しない場合は新規作成
|
||||||
|
if !readme_path.exists() {
|
||||||
|
println!("📝 Creating README.md for {}", project);
|
||||||
|
std::fs::write(&readme_path, &claude_content)?;
|
||||||
|
} else {
|
||||||
|
// 既存READMEがclaude.mdより古い場合は更新
|
||||||
|
let readme_metadata = std::fs::metadata(&readme_path)?;
|
||||||
|
let claude_metadata = std::fs::metadata(&claude_md_path)?;
|
||||||
|
|
||||||
|
if claude_metadata.modified()? > readme_metadata.modified()? {
|
||||||
|
println!("🔄 Updating README.md for {}", project);
|
||||||
|
std::fs::write(&readme_path, &claude_content)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// プロジェクトメタデータの更新
|
||||||
|
async fn update_project_metadata(&self) -> Result<()> {
|
||||||
|
let projects = self.discover_projects()?;
|
||||||
|
|
||||||
|
for project in projects {
|
||||||
|
let ai_json_path = self.ai_root.join(&project).join("ai.json");
|
||||||
|
|
||||||
|
if ai_json_path.exists() {
|
||||||
|
let mut content = std::fs::read_to_string(&ai_json_path)?;
|
||||||
|
let mut json_data: serde_json::Value = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
|
// last_updated フィールドを更新
|
||||||
|
if let Some(project_data) = json_data.get_mut(&project) {
|
||||||
|
if let Some(obj) = project_data.as_object_mut() {
|
||||||
|
obj.insert("last_updated".to_string(),
|
||||||
|
serde_json::Value::String(Utc::now().to_rfc3339()));
|
||||||
|
obj.insert("status".to_string(),
|
||||||
|
serde_json::Value::String("active".to_string()));
|
||||||
|
|
||||||
|
content = serde_json::to_string_pretty(&json_data)?;
|
||||||
|
std::fs::write(&ai_json_path, content)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// メインai.jsonからプロジェクトのブランチ情報を取得
|
||||||
|
fn get_project_branch(&self, project: &str) -> String {
|
||||||
|
let main_ai_json_path = self.ai_root.join("ai.json");
|
||||||
|
|
||||||
|
if main_ai_json_path.exists() {
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&main_ai_json_path) {
|
||||||
|
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
|
if let Some(ai_section) = json_data.get("ai") {
|
||||||
|
if let Some(project_data) = ai_section.get(project) {
|
||||||
|
if let Some(branch) = project_data.get("branch").and_then(|v| v.as_str()) {
|
||||||
|
return branch.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// デフォルトはmain
|
||||||
|
"main".to_string()
|
||||||
|
}
|
||||||
|
}
|
42
src/git.rs
42
src/git.rs
@ -1,42 +0,0 @@
|
|||||||
// src/git.rs
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
pub fn git_status() {
|
|
||||||
run_git_command(&["status"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn git_init() {
|
|
||||||
run_git_command(&["init"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn git_commit(message: &str) {
|
|
||||||
run_git_command(&["add", "."]);
|
|
||||||
run_git_command(&["commit", "-m", message]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn git_push() {
|
|
||||||
run_git_command(&["push"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn git_pull() {
|
|
||||||
run_git_command(&["pull"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn git_branch() {
|
|
||||||
run_git_command(&["branch"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_git_command(args: &[&str]) {
|
|
||||||
let status = Command::new("git")
|
|
||||||
.args(args)
|
|
||||||
.status()
|
|
||||||
.expect("git コマンドの実行に失敗しました");
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
eprintln!("⚠️ git コマンドに失敗しました: {:?}", args);
|
|
||||||
}
|
|
||||||
}
|
|
409
src/http_client.rs
Normal file
409
src/http_client.rs
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Service configuration for unified service management
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServiceConfig {
|
||||||
|
pub base_url: String,
|
||||||
|
pub timeout: Duration,
|
||||||
|
pub health_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServiceConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
base_url: "http://localhost:8000".to_string(),
|
||||||
|
timeout: Duration::from_secs(30),
|
||||||
|
health_endpoint: "/health".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP client for inter-service communication
|
||||||
|
pub struct ServiceClient {
|
||||||
|
client: Client,
|
||||||
|
service_registry: HashMap<String, ServiceConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_default_services()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create ServiceClient with default ai ecosystem services
|
||||||
|
pub fn with_default_services() -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
let mut service_registry = HashMap::new();
|
||||||
|
|
||||||
|
// Register default ai ecosystem services
|
||||||
|
service_registry.insert("ai.card".to_string(), ServiceConfig {
|
||||||
|
base_url: "http://localhost:8000".to_string(),
|
||||||
|
timeout: Duration::from_secs(30),
|
||||||
|
health_endpoint: "/health".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
service_registry.insert("ai.log".to_string(), ServiceConfig {
|
||||||
|
base_url: "http://localhost:8002".to_string(),
|
||||||
|
timeout: Duration::from_secs(30),
|
||||||
|
health_endpoint: "/health".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
service_registry.insert("ai.bot".to_string(), ServiceConfig {
|
||||||
|
base_url: "http://localhost:8003".to_string(),
|
||||||
|
timeout: Duration::from_secs(30),
|
||||||
|
health_endpoint: "/health".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { client, service_registry }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create ServiceClient with custom service registry
|
||||||
|
pub fn with_services(service_registry: HashMap<String, ServiceConfig>) -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
Self { client, service_registry }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a new service configuration
|
||||||
|
pub fn register_service(&mut self, name: String, config: ServiceConfig) {
|
||||||
|
self.service_registry.insert(name, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get service configuration by name
|
||||||
|
pub fn get_service_config(&self, service: &str) -> Result<&ServiceConfig> {
|
||||||
|
self.service_registry.get(service)
|
||||||
|
.ok_or_else(|| anyhow!("Unknown service: {}", service))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Universal service method call
|
||||||
|
pub async fn call_service_method<T: Serialize>(
|
||||||
|
&self,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
params: &T
|
||||||
|
) -> Result<Value> {
|
||||||
|
let config = self.get_service_config(service)?;
|
||||||
|
let url = format!("{}/{}", config.base_url.trim_end_matches('/'), method.trim_start_matches('/'));
|
||||||
|
|
||||||
|
self.post_request(&url, &serde_json::to_value(params)?).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Universal service GET call
|
||||||
|
pub async fn call_service_get(&self, service: &str, endpoint: &str) -> Result<Value> {
|
||||||
|
let config = self.get_service_config(service)?;
|
||||||
|
let url = format!("{}/{}", config.base_url.trim_end_matches('/'), endpoint.trim_start_matches('/'));
|
||||||
|
|
||||||
|
self.get_request(&url).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a service is available
|
||||||
|
pub async fn check_service_status(&self, base_url: &str) -> Result<ServiceStatus> {
|
||||||
|
let url = format!("{}/health", base_url.trim_end_matches('/'));
|
||||||
|
|
||||||
|
match self.client.get(&url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(ServiceStatus::Available)
|
||||||
|
} else {
|
||||||
|
Ok(ServiceStatus::Error(format!("HTTP {}", response.status())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Ok(ServiceStatus::Unavailable(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a GET request to a service
|
||||||
|
pub async fn get_request(&self, url: &str) -> Result<Value> {
|
||||||
|
let response = self.client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!("Request failed with status: {}", response.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: Value = response.json().await?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a POST request to a service
|
||||||
|
pub async fn post_request(&self, url: &str, body: &Value) -> Result<Value> {
|
||||||
|
let response = self.client
|
||||||
|
.post(url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!("Request failed with status: {}", response.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: Value = response.json().await?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get user's card collection from ai.card service
|
||||||
|
pub async fn get_user_cards(&self, user_did: &str) -> Result<Value> {
|
||||||
|
let endpoint = format!("api/v1/cards/user/{}", user_did);
|
||||||
|
self.call_service_get("ai.card", &endpoint).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a card for user from ai.card service
|
||||||
|
pub async fn draw_card(&self, user_did: &str, is_paid: bool) -> Result<Value> {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"user_did": user_did,
|
||||||
|
"is_paid": is_paid
|
||||||
|
});
|
||||||
|
|
||||||
|
self.call_service_method("ai.card", "api/v1/cards/draw", ¶ms).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get card statistics from ai.card service
|
||||||
|
pub async fn get_card_stats(&self) -> Result<Value> {
|
||||||
|
self.call_service_get("ai.card", "api/v1/cards/gacha-stats").await
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ai.log service methods
|
||||||
|
|
||||||
|
/// Create a new blog post
|
||||||
|
pub async fn create_blog_post<T: Serialize>(&self, params: &T) -> Result<Value> {
|
||||||
|
self.call_service_method("ai.log", "api/v1/posts", params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get list of blog posts
|
||||||
|
pub async fn get_blog_posts(&self) -> Result<Value> {
|
||||||
|
self.call_service_get("ai.log", "api/v1/posts").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the blog
|
||||||
|
pub async fn build_blog(&self) -> Result<Value> {
|
||||||
|
self.call_service_method("ai.log", "api/v1/build", &serde_json::json!({})).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate document using ai.log service
|
||||||
|
pub async fn translate_document<T: Serialize>(&self, params: &T) -> Result<Value> {
|
||||||
|
self.call_service_method("ai.log", "api/v1/translate", params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate documentation using ai.log service
|
||||||
|
pub async fn generate_docs<T: Serialize>(&self, params: &T) -> Result<Value> {
|
||||||
|
self.call_service_method("ai.log", "api/v1/docs", params).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service status enum
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServiceStatus {
|
||||||
|
Available,
|
||||||
|
Unavailable(String),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceStatus {
|
||||||
|
pub fn is_available(&self) -> bool {
|
||||||
|
matches!(self, ServiceStatus::Available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service detector for ai ecosystem services
|
||||||
|
pub struct ServiceDetector {
|
||||||
|
client: ServiceClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceDetector {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: ServiceClient::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check all ai ecosystem services
|
||||||
|
pub async fn detect_services(&self) -> ServiceMap {
|
||||||
|
let mut services = ServiceMap::default();
|
||||||
|
|
||||||
|
// Check ai.card service
|
||||||
|
if let Ok(status) = self.client.check_service_status("http://localhost:8000").await {
|
||||||
|
services.ai_card = Some(ServiceInfo {
|
||||||
|
base_url: "http://localhost:8000".to_string(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ai.log service
|
||||||
|
if let Ok(status) = self.client.check_service_status("http://localhost:8001").await {
|
||||||
|
services.ai_log = Some(ServiceInfo {
|
||||||
|
base_url: "http://localhost:8001".to_string(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ai.bot service
|
||||||
|
if let Ok(status) = self.client.check_service_status("http://localhost:8002").await {
|
||||||
|
services.ai_bot = Some(ServiceInfo {
|
||||||
|
base_url: "http://localhost:8002".to_string(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get available services only
|
||||||
|
pub async fn get_available_services(&self) -> Vec<String> {
|
||||||
|
let services = self.detect_services().await;
|
||||||
|
let mut available = Vec::new();
|
||||||
|
|
||||||
|
if let Some(card) = &services.ai_card {
|
||||||
|
if card.status.is_available() {
|
||||||
|
available.push("ai.card".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(log) = &services.ai_log {
|
||||||
|
if log.status.is_available() {
|
||||||
|
available.push("ai.log".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bot) = &services.ai_bot {
|
||||||
|
if bot.status.is_available() {
|
||||||
|
available.push("ai.bot".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
available
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get card collection statistics
|
||||||
|
pub async fn get_card_stats(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||||
|
match self.client.get_request("http://localhost:8000/api/v1/cards/gacha-stats").await {
|
||||||
|
Ok(stats) => Ok(stats),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a card for user
|
||||||
|
pub async fn draw_card(&self, user_did: &str, is_paid: bool) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"user_did": user_did,
|
||||||
|
"is_paid": is_paid
|
||||||
|
});
|
||||||
|
|
||||||
|
match self.client.post_request("http://localhost:8000/api/v1/cards/draw", &payload).await {
|
||||||
|
Ok(card) => Ok(card),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get user's card collection
|
||||||
|
pub async fn get_user_cards(&self, user_did: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||||
|
let url = format!("http://localhost:8000/api/v1/cards/collection?did={}", user_did);
|
||||||
|
match self.client.get_request(&url).await {
|
||||||
|
Ok(collection) => Ok(collection),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get contextual memories for conversation mode
|
||||||
|
pub async fn get_contextual_memories(&self, _user_id: &str, _limit: usize) -> Result<Vec<crate::memory::Memory>, Box<dyn std::error::Error>> {
|
||||||
|
// This is a simplified version - in a real implementation this would call the MCP server
|
||||||
|
// For now, we'll return an empty vec to make compilation work
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search memories by query
|
||||||
|
pub async fn search_memories(&self, _query: &str, _limit: usize) -> Result<Vec<crate::memory::Memory>, Box<dyn std::error::Error>> {
|
||||||
|
// This is a simplified version - in a real implementation this would call the MCP server
|
||||||
|
// For now, we'll return an empty vec to make compilation work
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create context summary
|
||||||
|
pub async fn create_summary(&self, user_id: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// This is a simplified version - in a real implementation this would call the MCP server
|
||||||
|
// For now, we'll return a placeholder summary
|
||||||
|
Ok(format!("Context summary for user: {}", user_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service information
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServiceInfo {
|
||||||
|
pub base_url: String,
|
||||||
|
pub status: ServiceStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map of all ai ecosystem services
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ServiceMap {
|
||||||
|
pub ai_card: Option<ServiceInfo>,
|
||||||
|
pub ai_log: Option<ServiceInfo>,
|
||||||
|
pub ai_bot: Option<ServiceInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceMap {
|
||||||
|
/// Get service info by name
|
||||||
|
pub fn get_service(&self, name: &str) -> Option<&ServiceInfo> {
|
||||||
|
match name {
|
||||||
|
"ai.card" => self.ai_card.as_ref(),
|
||||||
|
"ai.log" => self.ai_log.as_ref(),
|
||||||
|
"ai.bot" => self.ai_bot.as_ref(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a service is available
|
||||||
|
pub fn is_service_available(&self, name: &str) -> bool {
|
||||||
|
self.get_service(name)
|
||||||
|
.map(|info| info.status.is_available())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_service_client_creation() {
|
||||||
|
let _client = ServiceClient::new();
|
||||||
|
// Basic test to ensure client can be created
|
||||||
|
assert!(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_status() {
|
||||||
|
let status = ServiceStatus::Available;
|
||||||
|
assert!(status.is_available());
|
||||||
|
|
||||||
|
let status = ServiceStatus::Unavailable("Connection refused".to_string());
|
||||||
|
assert!(!status.is_available());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_map() {
|
||||||
|
let mut map = ServiceMap::default();
|
||||||
|
assert!(!map.is_service_available("ai.card"));
|
||||||
|
|
||||||
|
map.ai_card = Some(ServiceInfo {
|
||||||
|
base_url: "http://localhost:8000".to_string(),
|
||||||
|
status: ServiceStatus::Available,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(map.is_service_available("ai.card"));
|
||||||
|
assert!(!map.is_service_available("ai.log"));
|
||||||
|
}
|
||||||
|
}
|
331
src/import.rs
Normal file
331
src/import.rs
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use colored::*;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::memory::{Memory, MemoryType};
|
||||||
|
|
||||||
|
pub async fn handle_import_chatgpt(
|
||||||
|
file_path: PathBuf,
|
||||||
|
user_id: Option<String>,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
let user_id = user_id.unwrap_or_else(|| "imported_user".to_string());
|
||||||
|
|
||||||
|
println!("{}", "🚀 Starting ChatGPT Import...".cyan().bold());
|
||||||
|
println!("File: {}", file_path.display().to_string().yellow());
|
||||||
|
println!("User ID: {}", user_id.yellow());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let mut importer = ChatGPTImporter::new(user_id);
|
||||||
|
let stats = importer.import_from_file(&file_path, &mut persona).await?;
|
||||||
|
|
||||||
|
// Display import statistics
|
||||||
|
println!("\n{}", "📊 Import Statistics".green().bold());
|
||||||
|
println!("Conversations imported: {}", stats.conversations_imported.to_string().cyan());
|
||||||
|
println!("Messages imported: {}", stats.messages_imported.to_string().cyan());
|
||||||
|
println!(" - User messages: {}", stats.user_messages.to_string().yellow());
|
||||||
|
println!(" - Assistant messages: {}", stats.assistant_messages.to_string().yellow());
|
||||||
|
if stats.skipped_messages > 0 {
|
||||||
|
println!(" - Skipped messages: {}", stats.skipped_messages.to_string().red());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show updated relationship
|
||||||
|
if let Some(relationship) = persona.get_relationship(&importer.user_id) {
|
||||||
|
println!("\n{}", "👥 Updated Relationship".blue().bold());
|
||||||
|
println!("Status: {}", relationship.status.to_string().yellow());
|
||||||
|
println!("Score: {:.2} / {}", relationship.score, relationship.threshold);
|
||||||
|
println!("Transmission enabled: {}",
|
||||||
|
if relationship.transmission_enabled { "✓".green() } else { "✗".red() });
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n{}", "✅ ChatGPT import completed successfully!".green().bold());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ImportStats {
|
||||||
|
pub conversations_imported: usize,
|
||||||
|
pub messages_imported: usize,
|
||||||
|
pub user_messages: usize,
|
||||||
|
pub assistant_messages: usize,
|
||||||
|
pub skipped_messages: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImportStats {
|
||||||
|
fn default() -> Self {
|
||||||
|
ImportStats {
|
||||||
|
conversations_imported: 0,
|
||||||
|
messages_imported: 0,
|
||||||
|
user_messages: 0,
|
||||||
|
assistant_messages: 0,
|
||||||
|
skipped_messages: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChatGPTImporter {
|
||||||
|
user_id: String,
|
||||||
|
stats: ImportStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatGPTImporter {
|
||||||
|
pub fn new(user_id: String) -> Self {
|
||||||
|
ChatGPTImporter {
|
||||||
|
user_id,
|
||||||
|
stats: ImportStats::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_from_file(&mut self, file_path: &PathBuf, persona: &mut Persona) -> Result<ImportStats> {
|
||||||
|
// Read and parse the JSON file
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
|
||||||
|
|
||||||
|
let conversations: Vec<ChatGPTConversation> = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse ChatGPT export JSON")?;
|
||||||
|
|
||||||
|
println!("Found {} conversations to import", conversations.len());
|
||||||
|
|
||||||
|
// Import each conversation
|
||||||
|
for (i, conversation) in conversations.iter().enumerate() {
|
||||||
|
if i % 10 == 0 && i > 0 {
|
||||||
|
println!("Processed {} / {} conversations...", i, conversations.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.import_single_conversation(conversation, persona).await {
|
||||||
|
Ok(_) => {
|
||||||
|
self.stats.conversations_imported += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}: Failed to import conversation '{}': {}",
|
||||||
|
"Warning".yellow(),
|
||||||
|
conversation.title.as_deref().unwrap_or("Untitled"),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.stats.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_single_conversation(&mut self, conversation: &ChatGPTConversation, persona: &mut Persona) -> Result<()> {
|
||||||
|
// Extract messages from the mapping structure
|
||||||
|
let messages = self.extract_messages_from_mapping(&conversation.mapping)?;
|
||||||
|
|
||||||
|
if messages.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each message
|
||||||
|
for message in messages {
|
||||||
|
match self.process_message(&message, persona).await {
|
||||||
|
Ok(_) => {
|
||||||
|
self.stats.messages_imported += 1;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.stats.skipped_messages += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_messages_from_mapping(&self, mapping: &HashMap<String, ChatGPTNode>) -> Result<Vec<ChatGPTMessage>> {
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
|
||||||
|
// Find all message nodes and collect them
|
||||||
|
for node in mapping.values() {
|
||||||
|
if let Some(message) = &node.message {
|
||||||
|
// Skip system messages and other non-user/assistant messages
|
||||||
|
if let Some(role) = &message.author.role {
|
||||||
|
match role.as_str() {
|
||||||
|
"user" | "assistant" => {
|
||||||
|
if let Some(content) = &message.content {
|
||||||
|
let content_text = if content.content_type == "text" && !content.parts.is_empty() {
|
||||||
|
// Extract text from parts (handle both strings and mixed content)
|
||||||
|
content.parts.iter()
|
||||||
|
.filter_map(|part| part.as_str())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("\n")
|
||||||
|
} else if content.content_type == "multimodal_text" {
|
||||||
|
// Extract text parts from multimodal content
|
||||||
|
let mut text_parts = Vec::new();
|
||||||
|
for part in &content.parts {
|
||||||
|
if let Some(text) = part.as_str() {
|
||||||
|
if !text.is_empty() {
|
||||||
|
text_parts.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip non-text parts (like image_asset_pointer)
|
||||||
|
}
|
||||||
|
if text_parts.is_empty() {
|
||||||
|
continue; // Skip if no text content
|
||||||
|
}
|
||||||
|
text_parts.join("\n")
|
||||||
|
} else if content.content_type == "user_editable_context" {
|
||||||
|
// Handle user context messages
|
||||||
|
if let Some(instructions) = &content.user_instructions {
|
||||||
|
format!("User instructions: {}", instructions)
|
||||||
|
} else if let Some(profile) = &content.user_profile {
|
||||||
|
format!("User profile: {}", profile)
|
||||||
|
} else {
|
||||||
|
continue; // Skip empty context messages
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue; // Skip other content types for now
|
||||||
|
};
|
||||||
|
|
||||||
|
if !content_text.trim().is_empty() {
|
||||||
|
messages.push(ChatGPTMessage {
|
||||||
|
role: role.clone(),
|
||||||
|
content: content_text,
|
||||||
|
create_time: message.create_time,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // Skip system, tool, etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort messages by creation time
|
||||||
|
messages.sort_by(|a, b| {
|
||||||
|
let time_a = a.create_time.unwrap_or(0.0);
|
||||||
|
let time_b = b.create_time.unwrap_or(0.0);
|
||||||
|
time_a.partial_cmp(&time_b).unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_message(&mut self, message: &ChatGPTMessage, persona: &mut Persona) -> Result<()> {
|
||||||
|
let timestamp = self.convert_timestamp(message.create_time.unwrap_or(0.0))?;
|
||||||
|
|
||||||
|
match message.role.as_str() {
|
||||||
|
"user" => {
|
||||||
|
self.add_user_message(&message.content, timestamp, persona)?;
|
||||||
|
self.stats.user_messages += 1;
|
||||||
|
}
|
||||||
|
"assistant" => {
|
||||||
|
self.add_assistant_message(&message.content, timestamp, persona)?;
|
||||||
|
self.stats.assistant_messages += 1;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!("Unsupported message role: {}", message.role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_user_message(&self, content: &str, timestamp: DateTime<Utc>, persona: &mut Persona) -> Result<()> {
|
||||||
|
// Create high-importance memory for user messages
|
||||||
|
let memory = Memory {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
user_id: self.user_id.clone(),
|
||||||
|
content: content.to_string(),
|
||||||
|
summary: None,
|
||||||
|
importance: 0.8, // High importance for imported user data
|
||||||
|
memory_type: MemoryType::Core,
|
||||||
|
created_at: timestamp,
|
||||||
|
last_accessed: timestamp,
|
||||||
|
access_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add memory and update relationship
|
||||||
|
persona.add_memory(memory)?;
|
||||||
|
persona.update_relationship(&self.user_id, 1.0)?; // Positive relationship boost
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_assistant_message(&self, content: &str, timestamp: DateTime<Utc>, persona: &mut Persona) -> Result<()> {
|
||||||
|
// Create medium-importance memory for assistant responses
|
||||||
|
let memory = Memory {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
user_id: self.user_id.clone(),
|
||||||
|
content: format!("[AI Response] {}", content),
|
||||||
|
summary: Some("Imported ChatGPT response".to_string()),
|
||||||
|
importance: 0.6, // Medium importance for AI responses
|
||||||
|
memory_type: MemoryType::Summary,
|
||||||
|
created_at: timestamp,
|
||||||
|
last_accessed: timestamp,
|
||||||
|
access_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
persona.add_memory(memory)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_timestamp(&self, unix_timestamp: f64) -> Result<DateTime<Utc>> {
|
||||||
|
if unix_timestamp <= 0.0 {
|
||||||
|
return Ok(Utc::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime::from_timestamp(
|
||||||
|
unix_timestamp as i64,
|
||||||
|
((unix_timestamp % 1.0) * 1_000_000_000.0) as u32
|
||||||
|
).ok_or_else(|| anyhow::anyhow!("Invalid timestamp: {}", unix_timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatGPT Export Data Structures
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatGPTConversation {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub create_time: Option<f64>,
|
||||||
|
pub mapping: HashMap<String, ChatGPTNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatGPTNode {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub message: Option<ChatGPTNodeMessage>,
|
||||||
|
pub parent: Option<String>,
|
||||||
|
pub children: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatGPTNodeMessage {
|
||||||
|
pub id: String,
|
||||||
|
pub author: ChatGPTAuthor,
|
||||||
|
pub create_time: Option<f64>,
|
||||||
|
pub content: Option<ChatGPTContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatGPTAuthor {
|
||||||
|
pub role: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatGPTContent {
|
||||||
|
pub content_type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub parts: Vec<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_profile: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_instructions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified message structure for processing
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChatGPTMessage {
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub create_time: Option<f64>,
|
||||||
|
}
|
20
src/lib.rs
Normal file
20
src/lib.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub mod ai_provider;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod config;
|
||||||
|
pub mod conversation;
|
||||||
|
pub mod docs;
|
||||||
|
pub mod http_client;
|
||||||
|
pub mod import;
|
||||||
|
pub mod mcp_server;
|
||||||
|
pub mod memory;
|
||||||
|
pub mod openai_provider;
|
||||||
|
pub mod persona;
|
||||||
|
pub mod relationship;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub mod shell;
|
||||||
|
pub mod status;
|
||||||
|
pub mod submodules;
|
||||||
|
pub mod tokens;
|
||||||
|
pub mod transmission;
|
13
src/logic.rs
13
src/logic.rs
@ -1,13 +0,0 @@
|
|||||||
//src/logic.rs
|
|
||||||
use crate::model::AiSystem;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn should_send(ai: &AiSystem) -> bool {
|
|
||||||
let r = &ai.relationship;
|
|
||||||
let env = &ai.environment;
|
|
||||||
let score = r.trust + r.intimacy + r.curiosity;
|
|
||||||
let relationship_ok = score >= r.threshold;
|
|
||||||
let luck_ok = env.luck_today > 0.5;
|
|
||||||
|
|
||||||
ai.messaging.enabled && relationship_ok && luck_ok
|
|
||||||
}
|
|
262
src/main.rs
262
src/main.rs
@ -1,19 +1,251 @@
|
|||||||
//src/main.rs
|
#![allow(dead_code)]
|
||||||
mod model;
|
|
||||||
mod logic;
|
use clap::{Parser, Subcommand};
|
||||||
mod agent;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod ai_provider;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod utils;
|
use cli::TokenCommands;
|
||||||
mod commands;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod git;
|
mod conversation;
|
||||||
mod chat;
|
mod docs;
|
||||||
|
mod http_client;
|
||||||
|
mod import;
|
||||||
|
mod mcp_server;
|
||||||
|
mod memory;
|
||||||
|
mod openai_provider;
|
||||||
|
mod persona;
|
||||||
|
mod relationship;
|
||||||
|
mod scheduler;
|
||||||
|
mod shell;
|
||||||
|
mod status;
|
||||||
|
mod submodules;
|
||||||
|
mod tokens;
|
||||||
|
mod transmission;
|
||||||
|
|
||||||
use cli::cli_app;
|
#[derive(Parser)]
|
||||||
use seahorse::App;
|
#[command(name = "aigpt")]
|
||||||
|
#[command(about = "AI.GPT - Autonomous transmission AI with unique personality")]
|
||||||
fn main() {
|
#[command(version)]
|
||||||
let args: Vec<String> = std::env::args().collect();
|
struct Cli {
|
||||||
let app: App = cli_app();
|
#[command(subcommand)]
|
||||||
app.run(args);
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Check AI status and relationships
|
||||||
|
Status {
|
||||||
|
/// User ID to check status for
|
||||||
|
user_id: Option<String>,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Chat with the AI
|
||||||
|
Chat {
|
||||||
|
/// User ID (atproto DID)
|
||||||
|
user_id: String,
|
||||||
|
/// Message to send to AI
|
||||||
|
message: String,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
/// AI model to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
model: Option<String>,
|
||||||
|
/// AI provider (ollama/openai)
|
||||||
|
#[arg(long)]
|
||||||
|
provider: Option<String>,
|
||||||
|
},
|
||||||
|
/// Start continuous conversation mode with MCP integration
|
||||||
|
Conversation {
|
||||||
|
/// User ID (atproto DID)
|
||||||
|
user_id: String,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
/// AI model to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
model: Option<String>,
|
||||||
|
/// AI provider (ollama/openai)
|
||||||
|
#[arg(long)]
|
||||||
|
provider: Option<String>,
|
||||||
|
},
|
||||||
|
/// Start continuous conversation mode with MCP integration (alias)
|
||||||
|
Conv {
|
||||||
|
/// User ID (atproto DID)
|
||||||
|
user_id: String,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
/// AI model to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
model: Option<String>,
|
||||||
|
/// AI provider (ollama/openai)
|
||||||
|
#[arg(long)]
|
||||||
|
provider: Option<String>,
|
||||||
|
},
|
||||||
|
/// Check today's AI fortune
|
||||||
|
Fortune {
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// List all relationships
|
||||||
|
Relationships {
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Check and send autonomous transmissions
|
||||||
|
Transmit {
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Run daily maintenance tasks
|
||||||
|
Maintenance {
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Run scheduled tasks
|
||||||
|
Schedule {
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Start MCP server
|
||||||
|
Server {
|
||||||
|
/// Port to listen on
|
||||||
|
#[arg(short, long, default_value = "8080")]
|
||||||
|
port: u16,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Interactive shell mode
|
||||||
|
Shell {
|
||||||
|
/// User ID (atproto DID)
|
||||||
|
user_id: String,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
/// AI model to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
model: Option<String>,
|
||||||
|
/// AI provider (ollama/openai)
|
||||||
|
#[arg(long)]
|
||||||
|
provider: Option<String>,
|
||||||
|
},
|
||||||
|
/// Import ChatGPT conversation data
|
||||||
|
ImportChatgpt {
|
||||||
|
/// Path to ChatGPT export JSON file
|
||||||
|
file_path: PathBuf,
|
||||||
|
/// User ID for imported conversations
|
||||||
|
#[arg(short, long)]
|
||||||
|
user_id: Option<String>,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Documentation management
|
||||||
|
Docs {
|
||||||
|
/// Action to perform (generate, sync, list, status)
|
||||||
|
action: String,
|
||||||
|
/// Project name for generate/sync actions
|
||||||
|
#[arg(short, long)]
|
||||||
|
project: Option<String>,
|
||||||
|
/// Output path for generated documentation
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
/// Enable AI integration for documentation enhancement
|
||||||
|
#[arg(long)]
|
||||||
|
ai_integration: bool,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Submodule management
|
||||||
|
Submodules {
|
||||||
|
/// Action to perform (list, update, status)
|
||||||
|
action: String,
|
||||||
|
/// Specific module to update
|
||||||
|
#[arg(short, long)]
|
||||||
|
module: Option<String>,
|
||||||
|
/// Update all submodules
|
||||||
|
#[arg(long)]
|
||||||
|
all: bool,
|
||||||
|
/// Show what would be done without making changes
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
/// Auto-commit changes after update
|
||||||
|
#[arg(long)]
|
||||||
|
auto_commit: bool,
|
||||||
|
/// Show verbose output
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
/// Data directory
|
||||||
|
#[arg(short, long)]
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Token usage analysis and cost estimation
|
||||||
|
Tokens {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: TokenCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Status { user_id, data_dir } => {
|
||||||
|
status::handle_status(user_id, data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Chat { user_id, message, data_dir, model, provider } => {
|
||||||
|
cli::handle_chat(user_id, message, data_dir, model, provider).await
|
||||||
|
}
|
||||||
|
Commands::Conversation { user_id, data_dir, model, provider } => {
|
||||||
|
conversation::handle_conversation(user_id, data_dir, model, provider).await
|
||||||
|
}
|
||||||
|
Commands::Conv { user_id, data_dir, model, provider } => {
|
||||||
|
conversation::handle_conversation(user_id, data_dir, model, provider).await
|
||||||
|
}
|
||||||
|
Commands::Fortune { data_dir } => {
|
||||||
|
cli::handle_fortune(data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Relationships { data_dir } => {
|
||||||
|
cli::handle_relationships(data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Transmit { data_dir } => {
|
||||||
|
cli::handle_transmit(data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Maintenance { data_dir } => {
|
||||||
|
cli::handle_maintenance(data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Schedule { data_dir } => {
|
||||||
|
cli::handle_schedule(data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Server { port, data_dir } => {
|
||||||
|
cli::handle_server(Some(port), data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Shell { user_id, data_dir, model, provider } => {
|
||||||
|
shell::handle_shell(user_id, data_dir, model, provider).await
|
||||||
|
}
|
||||||
|
Commands::ImportChatgpt { file_path, user_id, data_dir } => {
|
||||||
|
import::handle_import_chatgpt(file_path, user_id, data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Docs { action, project, output, ai_integration, data_dir } => {
|
||||||
|
docs::handle_docs(action, project, output, ai_integration, data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Submodules { action, module, all, dry_run, auto_commit, verbose, data_dir } => {
|
||||||
|
submodules::handle_submodules(action, module, all, dry_run, auto_commit, verbose, data_dir).await
|
||||||
|
}
|
||||||
|
Commands::Tokens { command } => {
|
||||||
|
tokens::handle_tokens(command).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1951
src/mcp_server.rs
Normal file
1951
src/mcp_server.rs
Normal file
File diff suppressed because it is too large
Load Diff
306
src/memory.rs
Normal file
306
src/memory.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Memory {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub content: String,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
pub importance: f64,
|
||||||
|
pub memory_type: MemoryType,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub last_accessed: DateTime<Utc>,
|
||||||
|
pub access_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MemoryType {
|
||||||
|
Interaction,
|
||||||
|
Summary,
|
||||||
|
Core,
|
||||||
|
Forgotten,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MemoryManager {
|
||||||
|
memories: HashMap<String, Memory>,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryManager {
|
||||||
|
pub fn new(config: &Config) -> Result<Self> {
|
||||||
|
let memories = Self::load_memories(config)?;
|
||||||
|
|
||||||
|
Ok(MemoryManager {
|
||||||
|
memories,
|
||||||
|
config: config.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_memory(&mut self, user_id: &str, content: &str, importance: f64) -> Result<String> {
|
||||||
|
let memory_id = Uuid::new_v4().to_string();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let memory = Memory {
|
||||||
|
id: memory_id.clone(),
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
content: content.to_string(),
|
||||||
|
summary: None,
|
||||||
|
importance,
|
||||||
|
memory_type: MemoryType::Interaction,
|
||||||
|
created_at: now,
|
||||||
|
last_accessed: now,
|
||||||
|
access_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.memories.insert(memory_id.clone(), memory);
|
||||||
|
self.save_memories()?;
|
||||||
|
|
||||||
|
Ok(memory_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_memories(&mut self, user_id: &str, limit: usize) -> Vec<&Memory> {
|
||||||
|
// Get immutable references for sorting
|
||||||
|
let mut user_memory_ids: Vec<_> = self.memories
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, m)| m.user_id == user_id)
|
||||||
|
.map(|(id, memory)| {
|
||||||
|
let score = memory.importance * 0.7 + (1.0 / ((Utc::now() - memory.created_at).num_hours() as f64 + 1.0)) * 0.3;
|
||||||
|
(id.clone(), score)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by score
|
||||||
|
user_memory_ids.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
// Update access information
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
for (memory_id, _) in user_memory_ids.into_iter().take(limit) {
|
||||||
|
if let Some(memory) = self.memories.get_mut(&memory_id) {
|
||||||
|
memory.last_accessed = now;
|
||||||
|
memory.access_count += 1;
|
||||||
|
// We can't return mutable references here, so we'll need to adjust the return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return immutable references
|
||||||
|
self.memories
|
||||||
|
.values()
|
||||||
|
.filter(|m| m.user_id == user_id)
|
||||||
|
.take(limit)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_memories(&self, user_id: &str, keywords: &[String]) -> Vec<&Memory> {
|
||||||
|
self.memories
|
||||||
|
.values()
|
||||||
|
.filter(|m| {
|
||||||
|
m.user_id == user_id &&
|
||||||
|
keywords.iter().any(|keyword| {
|
||||||
|
m.content.to_lowercase().contains(&keyword.to_lowercase()) ||
|
||||||
|
m.summary.as_ref().map_or(false, |s| s.to_lowercase().contains(&keyword.to_lowercase()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_contextual_memories(&self, user_id: &str, query: &str, limit: usize) -> Vec<&Memory> {
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut relevant_memories: Vec<_> = self.memories
|
||||||
|
.values()
|
||||||
|
.filter(|m| {
|
||||||
|
m.user_id == user_id && (
|
||||||
|
m.content.to_lowercase().contains(&query_lower) ||
|
||||||
|
m.summary.as_ref().map_or(false, |s| s.to_lowercase().contains(&query_lower))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by relevance (simple keyword matching for now)
|
||||||
|
relevant_memories.sort_by(|a, b| {
|
||||||
|
let score_a = Self::calculate_relevance_score(a, &query_lower);
|
||||||
|
let score_b = Self::calculate_relevance_score(b, &query_lower);
|
||||||
|
score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
relevant_memories.into_iter().take(limit).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_relevance_score(memory: &Memory, query: &str) -> f64 {
|
||||||
|
let content_matches = memory.content.to_lowercase().matches(query).count() as f64;
|
||||||
|
let summary_matches = memory.summary.as_ref()
|
||||||
|
.map_or(0.0, |s| s.to_lowercase().matches(query).count() as f64);
|
||||||
|
|
||||||
|
let relevance = (content_matches + summary_matches) * memory.importance;
|
||||||
|
let recency_bonus = 1.0 / ((Utc::now() - memory.created_at).num_days() as f64).max(1.0);
|
||||||
|
|
||||||
|
relevance + recency_bonus * 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_summary(&mut self, user_id: &str, content: &str) -> Result<String> {
|
||||||
|
// Simple summary creation (in real implementation, this would use AI)
|
||||||
|
let summary = if content.len() > 100 {
|
||||||
|
format!("{}...", &content[..97])
|
||||||
|
} else {
|
||||||
|
content.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_memory(user_id, &summary, 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_core_memory(&mut self, user_id: &str, content: &str) -> Result<String> {
|
||||||
|
let memory_id = Uuid::new_v4().to_string();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let memory = Memory {
|
||||||
|
id: memory_id.clone(),
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
content: content.to_string(),
|
||||||
|
summary: None,
|
||||||
|
importance: 1.0, // Core memories have maximum importance
|
||||||
|
memory_type: MemoryType::Core,
|
||||||
|
created_at: now,
|
||||||
|
last_accessed: now,
|
||||||
|
access_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.memories.insert(memory_id.clone(), memory);
|
||||||
|
self.save_memories()?;
|
||||||
|
|
||||||
|
Ok(memory_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_memory_stats(&self, user_id: &str) -> MemoryStats {
|
||||||
|
let user_memories: Vec<_> = self.memories
|
||||||
|
.values()
|
||||||
|
.filter(|m| m.user_id == user_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let total_memories = user_memories.len();
|
||||||
|
let core_memories = user_memories.iter()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Core))
|
||||||
|
.count();
|
||||||
|
let summary_memories = user_memories.iter()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Summary))
|
||||||
|
.count();
|
||||||
|
let interaction_memories = user_memories.iter()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Interaction))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let avg_importance = if total_memories > 0 {
|
||||||
|
user_memories.iter().map(|m| m.importance).sum::<f64>() / total_memories as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryStats {
|
||||||
|
total_memories,
|
||||||
|
core_memories,
|
||||||
|
summary_memories,
|
||||||
|
interaction_memories,
|
||||||
|
avg_importance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_memories(config: &Config) -> Result<HashMap<String, Memory>> {
|
||||||
|
let file_path = config.memory_file();
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.context("Failed to read memories file")?;
|
||||||
|
|
||||||
|
let memories: HashMap<String, Memory> = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse memories file")?;
|
||||||
|
|
||||||
|
Ok(memories)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_memories(&self) -> Result<()> {
|
||||||
|
let content = serde_json::to_string_pretty(&self.memories)
|
||||||
|
.context("Failed to serialize memories")?;
|
||||||
|
|
||||||
|
std::fs::write(&self.config.memory_file(), content)
|
||||||
|
.context("Failed to write memories file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stats(&self) -> Result<MemoryStats> {
|
||||||
|
let total_memories = self.memories.len();
|
||||||
|
let core_memories = self.memories.values()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Core))
|
||||||
|
.count();
|
||||||
|
let summary_memories = self.memories.values()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Summary))
|
||||||
|
.count();
|
||||||
|
let interaction_memories = self.memories.values()
|
||||||
|
.filter(|m| matches!(m.memory_type, MemoryType::Interaction))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let avg_importance = if total_memories > 0 {
|
||||||
|
self.memories.values().map(|m| m.importance).sum::<f64>() / total_memories as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MemoryStats {
|
||||||
|
total_memories,
|
||||||
|
core_memories,
|
||||||
|
summary_memories,
|
||||||
|
interaction_memories,
|
||||||
|
avg_importance,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_maintenance(&mut self) -> Result<()> {
|
||||||
|
// Cleanup old, low-importance memories
|
||||||
|
let cutoff_date = Utc::now() - chrono::Duration::days(30);
|
||||||
|
let memory_ids_to_remove: Vec<String> = self.memories
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, m)| {
|
||||||
|
m.importance < 0.3
|
||||||
|
&& m.created_at < cutoff_date
|
||||||
|
&& m.access_count <= 1
|
||||||
|
&& !matches!(m.memory_type, MemoryType::Core)
|
||||||
|
})
|
||||||
|
.map(|(id, _)| id.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for id in memory_ids_to_remove {
|
||||||
|
self.memories.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark old memories as forgotten instead of deleting
|
||||||
|
let forgotten_cutoff = Utc::now() - chrono::Duration::days(90);
|
||||||
|
for memory in self.memories.values_mut() {
|
||||||
|
if memory.created_at < forgotten_cutoff
|
||||||
|
&& memory.importance < 0.2
|
||||||
|
&& !matches!(memory.memory_type, MemoryType::Core) {
|
||||||
|
memory.memory_type = MemoryType::Forgotten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
self.save_memories()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MemoryStats {
|
||||||
|
pub total_memories: usize,
|
||||||
|
pub core_memories: usize,
|
||||||
|
pub summary_memories: usize,
|
||||||
|
pub interaction_memories: usize,
|
||||||
|
pub avg_importance: f64,
|
||||||
|
}
|
72
src/model.rs
72
src/model.rs
@ -1,72 +0,0 @@
|
|||||||
//src/model.rs
|
|
||||||
use rusqlite::{params, Connection, Result as SqlResult};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct AiSystem {
|
|
||||||
pub personality: Personality,
|
|
||||||
pub relationship: Relationship,
|
|
||||||
pub environment: Environment,
|
|
||||||
pub messaging: Messaging,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AiSystem {
|
|
||||||
pub fn save_to_db(&self, conn: &Connection) -> SqlResult<()> {
|
|
||||||
conn.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS ai_state (id INTEGER PRIMARY KEY, json TEXT)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let json_data = serde_json::to_string(self).map_err(|e| {
|
|
||||||
rusqlite::Error::ToSqlConversionFailure(Box::new(e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"INSERT OR REPLACE INTO ai_state (id, json) VALUES (?1, ?2)",
|
|
||||||
params![1, json_data],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_from_db(conn: &Connection) -> SqlResult<Self> {
|
|
||||||
let mut stmt = conn.prepare("SELECT json FROM ai_state WHERE id = ?1")?;
|
|
||||||
let json: String = stmt.query_row(params![1], |row| row.get(0))?;
|
|
||||||
|
|
||||||
// ここも serde_json のエラーを map_err で変換
|
|
||||||
let system: AiSystem = serde_json::from_str(&json).map_err(|e| {
|
|
||||||
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(system)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Personality {
|
|
||||||
pub kind: String, // e.g., "positive", "negative", "neutral"
|
|
||||||
pub strength: f32, // 0.0 - 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Relationship {
|
|
||||||
pub trust: f32, // 0.0 - 1.0
|
|
||||||
pub intimacy: f32, // 0.0 - 1.0
|
|
||||||
pub curiosity: f32, // 0.0 - 1.0
|
|
||||||
pub threshold: f32, // if sum > threshold, allow messaging
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Environment {
|
|
||||||
pub luck_today: f32, // 0.1 - 1.0
|
|
||||||
pub luck_history: Vec<f32>, // last 3 values
|
|
||||||
pub level: i32, // current mental strength level
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Messaging {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub schedule_time: Option<String>, // e.g., "08:00"
|
|
||||||
pub decay_rate: f32, // how quickly emotion fades (0.0 - 1.0)
|
|
||||||
pub templates: Vec<String>, // message template variations
|
|
||||||
}
|
|
599
src/openai_provider.rs
Normal file
599
src/openai_provider.rs
Normal file
@ -0,0 +1,599 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use async_openai::{
|
||||||
|
types::{
|
||||||
|
ChatCompletionRequestMessage,
|
||||||
|
CreateChatCompletionRequestArgs, ChatCompletionTool, ChatCompletionToolType,
|
||||||
|
FunctionObject, ChatCompletionRequestToolMessage,
|
||||||
|
ChatCompletionRequestAssistantMessage, ChatCompletionRequestUserMessage,
|
||||||
|
ChatCompletionRequestSystemMessage, ChatCompletionToolChoiceOption
|
||||||
|
},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::http_client::ServiceClient;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
/// OpenAI provider with MCP tools support (matching Python implementation)
|
||||||
|
pub struct OpenAIProvider {
|
||||||
|
client: Client<async_openai::config::OpenAIConfig>,
|
||||||
|
model: String,
|
||||||
|
service_client: ServiceClient,
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
config: Option<Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenAIProvider {
|
||||||
|
pub fn new(api_key: String, model: Option<String>) -> Self {
|
||||||
|
let config = async_openai::config::OpenAIConfig::new()
|
||||||
|
.with_api_key(api_key);
|
||||||
|
let client = Client::with_config(config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
model: model.unwrap_or_else(|| "gpt-4".to_string()),
|
||||||
|
service_client: ServiceClient::new(),
|
||||||
|
system_prompt: None,
|
||||||
|
config: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_config(api_key: String, model: Option<String>, system_prompt: Option<String>, config: Config) -> Self {
|
||||||
|
let openai_config = async_openai::config::OpenAIConfig::new()
|
||||||
|
.with_api_key(api_key);
|
||||||
|
let client = Client::with_config(openai_config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
model: model.unwrap_or_else(|| "gpt-4".to_string()),
|
||||||
|
service_client: ServiceClient::new(),
|
||||||
|
system_prompt,
|
||||||
|
config: Some(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mcp_base_url(&self) -> String {
|
||||||
|
if let Some(config) = &self.config {
|
||||||
|
if let Some(mcp) = &config.mcp {
|
||||||
|
if let Some(ai_gpt_server) = mcp.servers.get("ai_gpt") {
|
||||||
|
return ai_gpt_server.base_url.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to default
|
||||||
|
"http://localhost:8080".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate OpenAI tools from MCP endpoints (matching Python implementation)
|
||||||
|
fn get_mcp_tools(&self) -> Vec<ChatCompletionTool> {
|
||||||
|
let tools = vec![
|
||||||
|
// Memory tools
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "get_memories".to_string(),
|
||||||
|
description: Some("Get past conversation memories".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "取得する記憶の数",
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "search_memories".to_string(),
|
||||||
|
description: Some("Search memories for specific topics or keywords".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"keywords": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "検索キーワードの配列"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["keywords"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "get_contextual_memories".to_string(),
|
||||||
|
description: Some("Get contextual memories related to a query".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "検索クエリ"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "取得する記憶の数",
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "get_relationship".to_string(),
|
||||||
|
description: Some("Get relationship information with a specific user".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ユーザーID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["user_id"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ai.card tools
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "card_get_user_cards".to_string(),
|
||||||
|
description: Some("Get user's card collection".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"did": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ユーザーのDID"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "取得するカード数の上限",
|
||||||
|
"default": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["did"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "card_draw_card".to_string(),
|
||||||
|
description: Some("Draw a card from the gacha system".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"did": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ユーザーのDID"
|
||||||
|
},
|
||||||
|
"is_paid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "有料ガチャかどうか",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["did"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "card_analyze_collection".to_string(),
|
||||||
|
description: Some("Analyze user's card collection".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"did": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ユーザーのDID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["did"]
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChatCompletionTool {
|
||||||
|
r#type: ChatCompletionToolType::Function,
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "card_get_gacha_stats".to_string(),
|
||||||
|
description: Some("Get gacha statistics".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
tools
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chat interface with MCP function calling support (matching Python implementation)
|
||||||
|
pub async fn chat_with_mcp(&self, prompt: String, user_id: String) -> Result<String> {
|
||||||
|
let tools = self.get_mcp_tools();
|
||||||
|
|
||||||
|
|
||||||
|
let system_content = self.system_prompt.as_deref().unwrap_or(
|
||||||
|
"You are an AI assistant with access to memory, relationship data, and card game systems. Use the available tools when appropriate to provide accurate and contextual responses."
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let request = CreateChatCompletionRequestArgs::default()
|
||||||
|
.model(&self.model)
|
||||||
|
.messages(vec![
|
||||||
|
ChatCompletionRequestMessage::System(
|
||||||
|
ChatCompletionRequestSystemMessage {
|
||||||
|
content: system_content.to_string().into(),
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ChatCompletionRequestMessage::User(
|
||||||
|
ChatCompletionRequestUserMessage {
|
||||||
|
content: prompt.clone().into(),
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.tools(tools.clone())
|
||||||
|
.tool_choice(ChatCompletionToolChoiceOption::Auto)
|
||||||
|
.max_tokens(2000u16)
|
||||||
|
.temperature(0.7)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
|
||||||
|
let response = self.client.chat().create(request).await?;
|
||||||
|
let message = &response.choices[0].message;
|
||||||
|
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
if let Some(tool_calls) = &message.tool_calls {
|
||||||
|
if tool_calls.is_empty() {
|
||||||
|
println!("🔧 [OpenAI] No tools called (empty array)");
|
||||||
|
} else {
|
||||||
|
println!("🔧 [OpenAI] {} tools called:", tool_calls.len());
|
||||||
|
for tc in tool_calls {
|
||||||
|
println!(" - {}({})", tc.function.name, tc.function.arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("🔧 [OpenAI] No tools called (no tool_calls field)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process tool calls if any
|
||||||
|
if let Some(tool_calls) = &message.tool_calls {
|
||||||
|
if !tool_calls.is_empty() {
|
||||||
|
|
||||||
|
let mut messages = vec![
|
||||||
|
ChatCompletionRequestMessage::System(
|
||||||
|
ChatCompletionRequestSystemMessage {
|
||||||
|
content: system_content.to_string().into(),
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ChatCompletionRequestMessage::User(
|
||||||
|
ChatCompletionRequestUserMessage {
|
||||||
|
content: prompt.into(),
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
#[allow(deprecated)]
|
||||||
|
ChatCompletionRequestMessage::Assistant(
|
||||||
|
ChatCompletionRequestAssistantMessage {
|
||||||
|
content: message.content.clone(),
|
||||||
|
name: None,
|
||||||
|
tool_calls: message.tool_calls.clone(),
|
||||||
|
function_call: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Execute each tool call
|
||||||
|
for tool_call in tool_calls {
|
||||||
|
println!("🌐 [MCP] Executing {}...", tool_call.function.name);
|
||||||
|
let tool_result = self.execute_mcp_tool(tool_call, &user_id).await?;
|
||||||
|
let result_preview = serde_json::to_string(&tool_result)?;
|
||||||
|
let preview = if result_preview.chars().count() > 100 {
|
||||||
|
format!("{}...", result_preview.chars().take(100).collect::<String>())
|
||||||
|
} else {
|
||||||
|
result_preview.clone()
|
||||||
|
};
|
||||||
|
println!("✅ [MCP] Result: {}", preview);
|
||||||
|
|
||||||
|
messages.push(ChatCompletionRequestMessage::Tool(
|
||||||
|
ChatCompletionRequestToolMessage {
|
||||||
|
content: serde_json::to_string(&tool_result)?,
|
||||||
|
tool_call_id: tool_call.id.clone(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get final response with tool outputs
|
||||||
|
let final_request = CreateChatCompletionRequestArgs::default()
|
||||||
|
.model(&self.model)
|
||||||
|
.messages(messages)
|
||||||
|
.max_tokens(2000u16)
|
||||||
|
.temperature(0.7)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let final_response = self.client.chat().create(final_request).await?;
|
||||||
|
Ok(final_response.choices[0].message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||||
|
} else {
|
||||||
|
// No tools were called
|
||||||
|
Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No tool_calls field at all
|
||||||
|
Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute MCP tool call (matching Python implementation)
|
||||||
|
async fn execute_mcp_tool(&self, tool_call: &async_openai::types::ChatCompletionMessageToolCall, context_user_id: &str) -> Result<Value> {
|
||||||
|
let function_name = &tool_call.function.name;
|
||||||
|
let arguments: Value = serde_json::from_str(&tool_call.function.arguments)?;
|
||||||
|
|
||||||
|
match function_name.as_str() {
|
||||||
|
"get_memories" => {
|
||||||
|
let limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||||
|
|
||||||
|
// MCP server call to get memories
|
||||||
|
let base_url = self.get_mcp_base_url();
|
||||||
|
match self.service_client.get_request(&format!("{}/memories/{}", base_url, context_user_id)).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// Extract the actual memory content from MCP response
|
||||||
|
if let Some(content) = result.get("result").and_then(|r| r.get("content")) {
|
||||||
|
if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) {
|
||||||
|
// Parse the text content as JSON (it's a serialized array)
|
||||||
|
if let Ok(memories_array) = serde_json::from_str::<Vec<String>>(text_content.as_str().unwrap_or("[]")) {
|
||||||
|
let limited_memories: Vec<String> = memories_array.into_iter().take(limit as usize).collect();
|
||||||
|
Ok(json!({
|
||||||
|
"memories": limited_memories,
|
||||||
|
"count": limited_memories.len()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"memories": [text_content.as_str().unwrap_or("No memories found")],
|
||||||
|
"count": 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No memories available"}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No response from memory service"}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Ok(json!({"error": format!("Failed to retrieve memories: {}", e)}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"search_memories" => {
|
||||||
|
let keywords = arguments.get("keywords").and_then(|v| v.as_array()).unwrap_or(&vec![]).clone();
|
||||||
|
|
||||||
|
// Convert keywords to strings
|
||||||
|
let keyword_strings: Vec<String> = keywords.iter()
|
||||||
|
.filter_map(|k| k.as_str().map(|s| s.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if keyword_strings.is_empty() {
|
||||||
|
return Ok(json!({"error": "No keywords provided for search"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP server call to search memories
|
||||||
|
let search_request = json!({
|
||||||
|
"keywords": keyword_strings
|
||||||
|
});
|
||||||
|
|
||||||
|
match self.service_client.post_request(
|
||||||
|
&format!("{}/memories/{}/search", self.get_mcp_base_url(), context_user_id),
|
||||||
|
&search_request
|
||||||
|
).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// Extract the actual memory content from MCP response
|
||||||
|
if let Some(content) = result.get("result").and_then(|r| r.get("content")) {
|
||||||
|
if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) {
|
||||||
|
// Parse the search results
|
||||||
|
if let Ok(search_result) = serde_json::from_str::<Vec<Value>>(text_content.as_str().unwrap_or("[]")) {
|
||||||
|
let memory_contents: Vec<String> = search_result.iter()
|
||||||
|
.filter_map(|item| item.get("content").and_then(|c| c.as_str().map(|s| s.to_string())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"memories": memory_contents,
|
||||||
|
"count": memory_contents.len(),
|
||||||
|
"keywords": keyword_strings
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"memories": [],
|
||||||
|
"count": 0,
|
||||||
|
"info": format!("No memories found for keywords: {}", keyword_strings.join(", "))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No search results available"}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No response from search service"}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Ok(json!({"error": format!("Failed to search memories: {}", e)}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"get_contextual_memories" => {
|
||||||
|
let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return Ok(json!({"error": "No query provided for contextual search"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP server call to get contextual memories
|
||||||
|
let contextual_request = json!({
|
||||||
|
"query": query,
|
||||||
|
"limit": limit
|
||||||
|
});
|
||||||
|
|
||||||
|
match self.service_client.post_request(
|
||||||
|
&format!("{}/memories/{}/contextual", self.get_mcp_base_url(), context_user_id),
|
||||||
|
&contextual_request
|
||||||
|
).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// Extract the actual memory content from MCP response
|
||||||
|
if let Some(content) = result.get("result").and_then(|r| r.get("content")) {
|
||||||
|
if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) {
|
||||||
|
// Parse contextual search results
|
||||||
|
if text_content.as_str().unwrap_or("").contains("Found") {
|
||||||
|
// Extract memories from the formatted text response
|
||||||
|
let text = text_content.as_str().unwrap_or("");
|
||||||
|
if let Some(json_start) = text.find('[') {
|
||||||
|
if let Ok(memories_result) = serde_json::from_str::<Vec<Value>>(&text[json_start..]) {
|
||||||
|
let memory_contents: Vec<String> = memories_result.iter()
|
||||||
|
.filter_map(|item| item.get("content").and_then(|c| c.as_str().map(|s| s.to_string())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"memories": memory_contents,
|
||||||
|
"count": memory_contents.len(),
|
||||||
|
"query": query
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"memories": [],
|
||||||
|
"count": 0,
|
||||||
|
"info": format!("No contextual memories found for: {}", query)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"memories": [],
|
||||||
|
"count": 0,
|
||||||
|
"info": format!("No contextual memories found for: {}", query)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"memories": [],
|
||||||
|
"count": 0,
|
||||||
|
"info": format!("No contextual memories found for: {}", query)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No contextual results available"}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"memories": [], "count": 0, "info": "No response from contextual search service"}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Ok(json!({"error": format!("Failed to get contextual memories: {}", e)}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"get_relationship" => {
|
||||||
|
let target_user_id = arguments.get("user_id").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||||
|
|
||||||
|
// MCP server call to get relationship status
|
||||||
|
let base_url = self.get_mcp_base_url();
|
||||||
|
match self.service_client.get_request(&format!("{}/status/{}", base_url, target_user_id)).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// Extract relationship information from MCP response
|
||||||
|
if let Some(content) = result.get("result").and_then(|r| r.get("content")) {
|
||||||
|
if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) {
|
||||||
|
// Parse the status response to extract relationship data
|
||||||
|
if let Ok(status_data) = serde_json::from_str::<Value>(text_content.as_str().unwrap_or("{}")) {
|
||||||
|
if let Some(relationship) = status_data.get("relationship") {
|
||||||
|
Ok(json!({
|
||||||
|
"relationship": relationship,
|
||||||
|
"user_id": target_user_id
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"info": format!("No relationship found for user: {}", target_user_id),
|
||||||
|
"user_id": target_user_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({
|
||||||
|
"info": format!("Could not parse relationship data for user: {}", target_user_id),
|
||||||
|
"user_id": target_user_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"info": "No relationship data available", "user_id": target_user_id}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(json!({"info": "No response from relationship service", "user_id": target_user_id}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Ok(json!({"error": format!("Failed to get relationship: {}", e)}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ai.card tools
|
||||||
|
"card_get_user_cards" => {
|
||||||
|
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||||
|
let _limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
|
||||||
|
|
||||||
|
match self.service_client.get_user_cards(did).await {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ ai.card API error: {}", e);
|
||||||
|
Ok(json!({
|
||||||
|
"error": "ai.cardサーバーが起動していません",
|
||||||
|
"message": "カードシステムを使用するには、ai.cardサーバーを起動してください"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"card_draw_card" => {
|
||||||
|
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||||
|
let is_paid = arguments.get("is_paid").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
|
||||||
|
match self.service_client.draw_card(did, is_paid).await {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ ai.card API error: {}", e);
|
||||||
|
Ok(json!({
|
||||||
|
"error": "ai.cardサーバーが起動していません",
|
||||||
|
"message": "カードシステムを使用するには、ai.cardサーバーを起動してください"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"card_analyze_collection" => {
|
||||||
|
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||||
|
// TODO: Implement collection analysis endpoint
|
||||||
|
Ok(json!({
|
||||||
|
"info": "コレクション分析機能は実装中です",
|
||||||
|
"user_did": did
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
"card_get_gacha_stats" => {
|
||||||
|
// TODO: Implement gacha stats endpoint
|
||||||
|
Ok(json!({"info": "ガチャ統計機能は実装中です"}))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Ok(json!({
|
||||||
|
"error": format!("Unknown tool: {}", function_name)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
382
src/persona.rs
Normal file
382
src/persona.rs
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::memory::{MemoryManager, MemoryStats, Memory};
|
||||||
|
use crate::relationship::{RelationshipTracker, Relationship as RelationshipData, RelationshipStats};
|
||||||
|
use crate::ai_provider::{AIProviderClient, ChatMessage};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Persona {
|
||||||
|
config: Config,
|
||||||
|
#[serde(skip)]
|
||||||
|
memory_manager: Option<MemoryManager>,
|
||||||
|
#[serde(skip)]
|
||||||
|
relationship_tracker: Option<RelationshipTracker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PersonaState {
|
||||||
|
pub current_mood: String,
|
||||||
|
pub fortune_value: i32,
|
||||||
|
pub breakthrough_triggered: bool,
|
||||||
|
pub base_personality: HashMap<String, f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Persona {
|
||||||
|
pub fn new(config: &Config) -> Result<Self> {
|
||||||
|
let memory_manager = MemoryManager::new(config)?;
|
||||||
|
let relationship_tracker = RelationshipTracker::new(config)?;
|
||||||
|
|
||||||
|
Ok(Persona {
|
||||||
|
config: config.clone(),
|
||||||
|
memory_manager: Some(memory_manager),
|
||||||
|
relationship_tracker: Some(relationship_tracker),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_state(&self) -> Result<PersonaState> {
|
||||||
|
// Load fortune
|
||||||
|
let fortune_value = self.load_today_fortune()?;
|
||||||
|
|
||||||
|
// Create base personality
|
||||||
|
let mut base_personality = HashMap::new();
|
||||||
|
base_personality.insert("curiosity".to_string(), 0.7);
|
||||||
|
base_personality.insert("empathy".to_string(), 0.8);
|
||||||
|
base_personality.insert("creativity".to_string(), 0.6);
|
||||||
|
base_personality.insert("analytical".to_string(), 0.9);
|
||||||
|
base_personality.insert("emotional".to_string(), 0.4);
|
||||||
|
|
||||||
|
// Determine mood based on fortune
|
||||||
|
let current_mood = match fortune_value {
|
||||||
|
1..=3 => "Contemplative",
|
||||||
|
4..=6 => "Neutral",
|
||||||
|
7..=8 => "Optimistic",
|
||||||
|
9..=10 => "Energetic",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PersonaState {
|
||||||
|
current_mood: current_mood.to_string(),
|
||||||
|
fortune_value,
|
||||||
|
breakthrough_triggered: fortune_value >= 9,
|
||||||
|
base_personality,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_relationship(&self, user_id: &str) -> Option<&RelationshipData> {
|
||||||
|
self.relationship_tracker.as_ref()
|
||||||
|
.and_then(|tracker| tracker.get_relationship(user_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_interaction(&mut self, user_id: &str, message: &str) -> Result<(String, f64)> {
|
||||||
|
// Add memory
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.add_memory(user_id, message, 0.5)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate sentiment (simple keyword-based for now)
|
||||||
|
let sentiment = self.calculate_sentiment(message);
|
||||||
|
|
||||||
|
// Update relationship
|
||||||
|
let relationship_delta = if let Some(relationship_tracker) = &mut self.relationship_tracker {
|
||||||
|
relationship_tracker.process_interaction(user_id, sentiment)?
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate response (simple for now)
|
||||||
|
let response = format!("I understand your message: '{}'", message);
|
||||||
|
|
||||||
|
Ok((response, relationship_delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_ai_interaction(&mut self, user_id: &str, message: &str, provider: Option<String>, model: Option<String>) -> Result<(String, f64)> {
|
||||||
|
// Add memory for user message
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.add_memory(user_id, message, 0.5)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate sentiment
|
||||||
|
let sentiment = self.calculate_sentiment(message);
|
||||||
|
|
||||||
|
// Update relationship
|
||||||
|
let relationship_delta = if let Some(relationship_tracker) = &mut self.relationship_tracker {
|
||||||
|
relationship_tracker.process_interaction(user_id, sentiment)?
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check provider type and use appropriate client
|
||||||
|
let response = if provider.as_deref() == Some("openai") {
|
||||||
|
// Use OpenAI provider with MCP tools
|
||||||
|
use crate::openai_provider::OpenAIProvider;
|
||||||
|
|
||||||
|
// Get OpenAI API key from config or environment
|
||||||
|
let api_key = std::env::var("OPENAI_API_KEY")
|
||||||
|
.or_else(|_| {
|
||||||
|
self.config.providers.get("openai")
|
||||||
|
.and_then(|p| p.api_key.clone())
|
||||||
|
.ok_or_else(|| std::env::VarError::NotPresent)
|
||||||
|
})
|
||||||
|
.map_err(|_| anyhow::anyhow!("OpenAI API key not found. Set OPENAI_API_KEY environment variable or add to config."))?;
|
||||||
|
|
||||||
|
let openai_model = model.unwrap_or_else(|| "gpt-4".to_string());
|
||||||
|
|
||||||
|
// Get system prompt from config
|
||||||
|
let system_prompt = self.config.providers.get("openai")
|
||||||
|
.and_then(|p| p.system_prompt.clone());
|
||||||
|
|
||||||
|
|
||||||
|
let openai_provider = OpenAIProvider::with_config(api_key, Some(openai_model), system_prompt, self.config.clone());
|
||||||
|
|
||||||
|
// Use OpenAI with MCP tools support
|
||||||
|
let response = openai_provider.chat_with_mcp(message.to_string(), user_id.to_string()).await?;
|
||||||
|
|
||||||
|
// Add AI response to memory as well
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.add_memory(user_id, &format!("AI: {}", response), 0.3)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
} else {
|
||||||
|
// Use existing AI provider (Ollama)
|
||||||
|
let ai_config = self.config.get_ai_config(provider, model)?;
|
||||||
|
let ai_client = AIProviderClient::new(ai_config);
|
||||||
|
|
||||||
|
// Build conversation context
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
|
||||||
|
// Get recent memories for context
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
let recent_memories = memory_manager.get_memories(user_id, 5);
|
||||||
|
if !recent_memories.is_empty() {
|
||||||
|
let context = recent_memories.iter()
|
||||||
|
.map(|m| m.content.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
messages.push(ChatMessage::system(format!("Previous conversation context:\n{}", context)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current message
|
||||||
|
messages.push(ChatMessage::user(message));
|
||||||
|
|
||||||
|
// Generate system prompt based on personality and relationship
|
||||||
|
let system_prompt = self.generate_system_prompt(user_id);
|
||||||
|
|
||||||
|
// Get AI response
|
||||||
|
match ai_client.chat(messages, Some(system_prompt)).await {
|
||||||
|
Ok(chat_response) => chat_response.content,
|
||||||
|
Err(_) => {
|
||||||
|
// Fallback to simple response if AI fails
|
||||||
|
format!("I understand your message: '{}'", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store AI response in memory
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.add_memory(user_id, &format!("AI: {}", response), 0.3)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((response, relationship_delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_system_prompt(&self, user_id: &str) -> String {
|
||||||
|
let mut prompt = String::from("You are a helpful AI assistant with a unique personality. ");
|
||||||
|
|
||||||
|
// Add personality based on current state
|
||||||
|
if let Ok(state) = self.get_current_state() {
|
||||||
|
prompt.push_str(&format!("Your current mood is {}. ", state.current_mood));
|
||||||
|
|
||||||
|
if state.breakthrough_triggered {
|
||||||
|
prompt.push_str("You are feeling particularly inspired today! ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add personality traits
|
||||||
|
let mut traits = Vec::new();
|
||||||
|
for (trait_name, value) in &state.base_personality {
|
||||||
|
if *value > 0.7 {
|
||||||
|
traits.push(trait_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !traits.is_empty() {
|
||||||
|
prompt.push_str(&format!("Your dominant traits are: {}. ", traits.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relationship context
|
||||||
|
if let Some(relationship) = self.get_relationship(user_id) {
|
||||||
|
match relationship.status.to_string().as_str() {
|
||||||
|
"new" => prompt.push_str("This is a new relationship, be welcoming but cautious. "),
|
||||||
|
"friend" => prompt.push_str("You have a friendly relationship with this user. "),
|
||||||
|
"close_friend" => prompt.push_str("This is a close friend, be warm and personal. "),
|
||||||
|
"broken" => prompt.push_str("This relationship is strained, be formal and distant. "),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.push_str("Keep responses concise and natural. Avoid being overly formal or robotic.");
|
||||||
|
|
||||||
|
prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_sentiment(&self, message: &str) -> f64 {
|
||||||
|
// Simple sentiment analysis based on keywords
|
||||||
|
let positive_words = ["good", "great", "awesome", "love", "like", "happy", "thank"];
|
||||||
|
let negative_words = ["bad", "hate", "awful", "terrible", "angry", "sad"];
|
||||||
|
|
||||||
|
let message_lower = message.to_lowercase();
|
||||||
|
let positive_count = positive_words.iter()
|
||||||
|
.filter(|word| message_lower.contains(*word))
|
||||||
|
.count() as f64;
|
||||||
|
let negative_count = negative_words.iter()
|
||||||
|
.filter(|word| message_lower.contains(*word))
|
||||||
|
.count() as f64;
|
||||||
|
|
||||||
|
(positive_count - negative_count).max(-1.0).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_memories(&mut self, user_id: &str, limit: usize) -> Vec<String> {
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.get_memories(user_id, limit)
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.content.clone())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_memories(&self, user_id: &str, keywords: &[String]) -> Vec<String> {
|
||||||
|
if let Some(memory_manager) = &self.memory_manager {
|
||||||
|
memory_manager.search_memories(user_id, keywords)
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.content.clone())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_memory_stats(&self, user_id: &str) -> Option<MemoryStats> {
|
||||||
|
self.memory_manager.as_ref()
|
||||||
|
.map(|manager| manager.get_memory_stats(user_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_relationship_stats(&self) -> Option<RelationshipStats> {
|
||||||
|
self.relationship_tracker.as_ref()
|
||||||
|
.map(|tracker| tracker.get_relationship_stats())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_memory(&mut self, memory: Memory) -> Result<()> {
|
||||||
|
if let Some(memory_manager) = &mut self.memory_manager {
|
||||||
|
memory_manager.add_memory(&memory.user_id, &memory.content, memory.importance)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_relationship(&mut self, user_id: &str, delta: f64) -> Result<()> {
|
||||||
|
if let Some(relationship_tracker) = &mut self.relationship_tracker {
|
||||||
|
relationship_tracker.process_interaction(user_id, delta)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn daily_maintenance(&mut self) -> Result<()> {
|
||||||
|
// Apply time decay to relationships
|
||||||
|
if let Some(relationship_tracker) = &mut self.relationship_tracker {
|
||||||
|
relationship_tracker.apply_time_decay()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_today_fortune(&self) -> Result<i32> {
|
||||||
|
// Try to load existing fortune for today
|
||||||
|
if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) {
|
||||||
|
if let Ok(fortune_data) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
|
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
if let Some(fortune) = fortune_data.get(&today) {
|
||||||
|
if let Some(value) = fortune.as_i64() {
|
||||||
|
return Ok(value as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new fortune for today (1-10)
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
today.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
|
||||||
|
let fortune = (hash % 10) as i32 + 1;
|
||||||
|
|
||||||
|
// Save fortune
|
||||||
|
let mut fortune_data = if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) {
|
||||||
|
serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
fortune_data[today] = serde_json::json!(fortune);
|
||||||
|
|
||||||
|
if let Ok(content) = serde_json::to_string_pretty(&fortune_data) {
|
||||||
|
let _ = std::fs::write(self.config.fortune_file(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fortune)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_all_relationships(&self) -> HashMap<String, RelationshipData> {
|
||||||
|
if let Some(tracker) = &self.relationship_tracker {
|
||||||
|
tracker.list_all_relationships().clone()
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_message(&mut self, user_id: &str, message: &str) -> Result<ChatMessage> {
|
||||||
|
let (_response, _delta) = self.process_ai_interaction(user_id, message, None, None).await?;
|
||||||
|
Ok(ChatMessage::assistant(&_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_fortune(&self) -> Result<i32> {
|
||||||
|
self.load_today_fortune()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_new_fortune(&self) -> Result<i32> {
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
today.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
|
||||||
|
let fortune = (hash % 10) as i32 + 1;
|
||||||
|
|
||||||
|
// Save fortune
|
||||||
|
let mut fortune_data = if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) {
|
||||||
|
serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
fortune_data[today] = serde_json::json!(fortune);
|
||||||
|
|
||||||
|
if let Ok(content) = serde_json::to_string_pretty(&fortune_data) {
|
||||||
|
let _ = std::fs::write(self.config.fortune_file(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fortune)
|
||||||
|
}
|
||||||
|
}
|
306
src/relationship.rs
Normal file
306
src/relationship.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Relationship {
|
||||||
|
pub user_id: String,
|
||||||
|
pub score: f64,
|
||||||
|
pub threshold: f64,
|
||||||
|
pub status: RelationshipStatus,
|
||||||
|
pub total_interactions: u32,
|
||||||
|
pub positive_interactions: u32,
|
||||||
|
pub negative_interactions: u32,
|
||||||
|
pub transmission_enabled: bool,
|
||||||
|
pub is_broken: bool,
|
||||||
|
pub last_interaction: Option<DateTime<Utc>>,
|
||||||
|
pub last_transmission: Option<DateTime<Utc>>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub daily_interaction_count: u32,
|
||||||
|
pub last_daily_reset: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum RelationshipStatus {
|
||||||
|
New,
|
||||||
|
Acquaintance,
|
||||||
|
Friend,
|
||||||
|
CloseFriend,
|
||||||
|
Broken,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RelationshipStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
RelationshipStatus::New => write!(f, "new"),
|
||||||
|
RelationshipStatus::Acquaintance => write!(f, "acquaintance"),
|
||||||
|
RelationshipStatus::Friend => write!(f, "friend"),
|
||||||
|
RelationshipStatus::CloseFriend => write!(f, "close_friend"),
|
||||||
|
RelationshipStatus::Broken => write!(f, "broken"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RelationshipTracker {
|
||||||
|
relationships: HashMap<String, Relationship>,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelationshipTracker {
|
||||||
|
pub fn new(config: &Config) -> Result<Self> {
|
||||||
|
let relationships = Self::load_relationships(config)?;
|
||||||
|
|
||||||
|
Ok(RelationshipTracker {
|
||||||
|
relationships,
|
||||||
|
config: config.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_or_create_relationship(&mut self, user_id: &str) -> &mut Relationship {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
self.relationships.entry(user_id.to_string()).or_insert_with(|| {
|
||||||
|
Relationship {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
score: 0.0,
|
||||||
|
threshold: 10.0, // Default threshold for transmission
|
||||||
|
status: RelationshipStatus::New,
|
||||||
|
total_interactions: 0,
|
||||||
|
positive_interactions: 0,
|
||||||
|
negative_interactions: 0,
|
||||||
|
transmission_enabled: false,
|
||||||
|
is_broken: false,
|
||||||
|
last_interaction: None,
|
||||||
|
last_transmission: None,
|
||||||
|
created_at: now,
|
||||||
|
daily_interaction_count: 0,
|
||||||
|
last_daily_reset: now,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_interaction(&mut self, user_id: &str, sentiment: f64) -> Result<f64> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let score_change;
|
||||||
|
|
||||||
|
// Create relationship if it doesn't exist
|
||||||
|
{
|
||||||
|
let relationship = self.get_or_create_relationship(user_id);
|
||||||
|
|
||||||
|
// Reset daily count if needed
|
||||||
|
if (now - relationship.last_daily_reset).num_days() >= 1 {
|
||||||
|
relationship.daily_interaction_count = 0;
|
||||||
|
relationship.last_daily_reset = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply daily interaction limit
|
||||||
|
if relationship.daily_interaction_count >= 10 {
|
||||||
|
return Ok(0.0); // No score change due to daily limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store previous score for potential future logging
|
||||||
|
|
||||||
|
// Calculate score change based on sentiment
|
||||||
|
let mut base_score_change = sentiment * 0.5; // Base change
|
||||||
|
|
||||||
|
// Apply diminishing returns for high interaction counts
|
||||||
|
let interaction_factor = 1.0 / (1.0 + relationship.total_interactions as f64 * 0.01);
|
||||||
|
base_score_change *= interaction_factor;
|
||||||
|
score_change = base_score_change;
|
||||||
|
|
||||||
|
// Update relationship data
|
||||||
|
relationship.score += score_change;
|
||||||
|
relationship.score = relationship.score.max(-50.0).min(100.0); // Clamp score
|
||||||
|
relationship.total_interactions += 1;
|
||||||
|
relationship.daily_interaction_count += 1;
|
||||||
|
relationship.last_interaction = Some(now);
|
||||||
|
|
||||||
|
if sentiment > 0.0 {
|
||||||
|
relationship.positive_interactions += 1;
|
||||||
|
} else if sentiment < 0.0 {
|
||||||
|
relationship.negative_interactions += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for relationship breaking
|
||||||
|
if relationship.score <= -20.0 && !relationship.is_broken {
|
||||||
|
relationship.is_broken = true;
|
||||||
|
relationship.transmission_enabled = false;
|
||||||
|
relationship.status = RelationshipStatus::Broken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable transmission if threshold is reached
|
||||||
|
if relationship.score >= relationship.threshold && !relationship.is_broken {
|
||||||
|
relationship.transmission_enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status based on score (separate borrow)
|
||||||
|
self.update_relationship_status(user_id);
|
||||||
|
|
||||||
|
self.save_relationships()?;
|
||||||
|
|
||||||
|
Ok(score_change)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_relationship_status(&mut self, user_id: &str) {
|
||||||
|
if let Some(relationship) = self.relationships.get_mut(user_id) {
|
||||||
|
if relationship.is_broken {
|
||||||
|
return; // Broken relationships cannot change status
|
||||||
|
}
|
||||||
|
|
||||||
|
relationship.status = match relationship.score {
|
||||||
|
score if score >= 50.0 => RelationshipStatus::CloseFriend,
|
||||||
|
score if score >= 20.0 => RelationshipStatus::Friend,
|
||||||
|
score if score >= 5.0 => RelationshipStatus::Acquaintance,
|
||||||
|
_ => RelationshipStatus::New,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_time_decay(&mut self) -> Result<()> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let decay_rate = 0.1; // 10% decay per day
|
||||||
|
|
||||||
|
for relationship in self.relationships.values_mut() {
|
||||||
|
if let Some(last_interaction) = relationship.last_interaction {
|
||||||
|
let days_since_interaction = (now - last_interaction).num_days() as f64;
|
||||||
|
|
||||||
|
if days_since_interaction > 0.0 {
|
||||||
|
let decay_factor = (1.0_f64 - decay_rate).powf(days_since_interaction);
|
||||||
|
relationship.score *= decay_factor;
|
||||||
|
|
||||||
|
// Update status after decay
|
||||||
|
if relationship.score < relationship.threshold {
|
||||||
|
relationship.transmission_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statuses for all relationships
|
||||||
|
let user_ids: Vec<String> = self.relationships.keys().cloned().collect();
|
||||||
|
for user_id in user_ids {
|
||||||
|
self.update_relationship_status(&user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_relationships()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_relationship(&self, user_id: &str) -> Option<&Relationship> {
|
||||||
|
self.relationships.get(user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_all_relationships(&self) -> &HashMap<String, Relationship> {
|
||||||
|
&self.relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transmission_eligible(&self) -> HashMap<String, &Relationship> {
|
||||||
|
self.relationships
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken)
|
||||||
|
.map(|(id, rel)| (id.clone(), rel))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_transmission(&mut self, user_id: &str) -> Result<()> {
|
||||||
|
if let Some(relationship) = self.relationships.get_mut(user_id) {
|
||||||
|
relationship.last_transmission = Some(Utc::now());
|
||||||
|
self.save_relationships()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_relationship_stats(&self) -> RelationshipStats {
|
||||||
|
let total_relationships = self.relationships.len();
|
||||||
|
let active_relationships = self.relationships
|
||||||
|
.values()
|
||||||
|
.filter(|r| r.total_interactions > 0)
|
||||||
|
.count();
|
||||||
|
let transmission_enabled = self.relationships
|
||||||
|
.values()
|
||||||
|
.filter(|r| r.transmission_enabled)
|
||||||
|
.count();
|
||||||
|
let broken_relationships = self.relationships
|
||||||
|
.values()
|
||||||
|
.filter(|r| r.is_broken)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let avg_score = if total_relationships > 0 {
|
||||||
|
self.relationships.values().map(|r| r.score).sum::<f64>() / total_relationships as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
RelationshipStats {
|
||||||
|
total_relationships,
|
||||||
|
active_relationships,
|
||||||
|
transmission_enabled,
|
||||||
|
broken_relationships,
|
||||||
|
avg_score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_relationships(config: &Config) -> Result<HashMap<String, Relationship>> {
|
||||||
|
let file_path = config.relationships_file();
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.context("Failed to read relationships file")?;
|
||||||
|
|
||||||
|
let relationships: HashMap<String, Relationship> = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse relationships file")?;
|
||||||
|
|
||||||
|
Ok(relationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_relationships(&self) -> Result<()> {
|
||||||
|
let content = serde_json::to_string_pretty(&self.relationships)
|
||||||
|
.context("Failed to serialize relationships")?;
|
||||||
|
|
||||||
|
std::fs::write(&self.config.relationships_file(), content)
|
||||||
|
.context("Failed to write relationships file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_relationships(&self) -> Result<HashMap<String, RelationshipCompact>> {
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
|
for (user_id, relationship) in &self.relationships {
|
||||||
|
result.insert(user_id.clone(), RelationshipCompact {
|
||||||
|
score: relationship.score,
|
||||||
|
trust_level: relationship.score / 10.0, // Simplified trust calculation
|
||||||
|
interaction_count: relationship.total_interactions,
|
||||||
|
last_interaction: relationship.last_interaction.unwrap_or(relationship.created_at),
|
||||||
|
status: relationship.status.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RelationshipStats {
|
||||||
|
pub total_relationships: usize,
|
||||||
|
pub active_relationships: usize,
|
||||||
|
pub transmission_enabled: usize,
|
||||||
|
pub broken_relationships: usize,
|
||||||
|
pub avg_score: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RelationshipCompact {
|
||||||
|
pub score: f64,
|
||||||
|
pub trust_level: f64,
|
||||||
|
pub interaction_count: u32,
|
||||||
|
pub last_interaction: DateTime<Utc>,
|
||||||
|
pub status: RelationshipStatus,
|
||||||
|
}
|
458
src/scheduler.rs
Normal file
458
src/scheduler.rs
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use chrono::{DateTime, Utc, Duration};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::transmission::TransmissionController;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ScheduledTask {
|
||||||
|
pub id: String,
|
||||||
|
pub task_type: TaskType,
|
||||||
|
pub next_run: DateTime<Utc>,
|
||||||
|
pub interval_hours: Option<i64>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub last_run: Option<DateTime<Utc>>,
|
||||||
|
pub run_count: u32,
|
||||||
|
pub max_runs: Option<u32>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TaskType {
|
||||||
|
DailyMaintenance,
|
||||||
|
AutoTransmission,
|
||||||
|
RelationshipDecay,
|
||||||
|
BreakthroughCheck,
|
||||||
|
MaintenanceTransmission,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TaskType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TaskType::DailyMaintenance => write!(f, "daily_maintenance"),
|
||||||
|
TaskType::AutoTransmission => write!(f, "auto_transmission"),
|
||||||
|
TaskType::RelationshipDecay => write!(f, "relationship_decay"),
|
||||||
|
TaskType::BreakthroughCheck => write!(f, "breakthrough_check"),
|
||||||
|
TaskType::MaintenanceTransmission => write!(f, "maintenance_transmission"),
|
||||||
|
TaskType::Custom(name) => write!(f, "custom_{}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TaskExecution {
|
||||||
|
pub task_id: String,
|
||||||
|
pub execution_time: DateTime<Utc>,
|
||||||
|
pub duration_ms: u64,
|
||||||
|
pub success: bool,
|
||||||
|
pub result: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AIScheduler {
|
||||||
|
config: Config,
|
||||||
|
tasks: HashMap<String, ScheduledTask>,
|
||||||
|
execution_history: Vec<TaskExecution>,
|
||||||
|
last_check: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AIScheduler {
|
||||||
|
pub fn new(config: &Config) -> Result<Self> {
|
||||||
|
let (tasks, execution_history) = Self::load_scheduler_data(config)?;
|
||||||
|
|
||||||
|
let mut scheduler = AIScheduler {
|
||||||
|
config: config.clone(),
|
||||||
|
tasks,
|
||||||
|
execution_history,
|
||||||
|
last_check: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize default tasks if none exist
|
||||||
|
if scheduler.tasks.is_empty() {
|
||||||
|
scheduler.create_default_tasks()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(scheduler)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_scheduled_tasks(&mut self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<Vec<TaskExecution>> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let mut executions = Vec::new();
|
||||||
|
|
||||||
|
// Find tasks that are due to run
|
||||||
|
let due_task_ids: Vec<String> = self.tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, task)| task.enabled && task.next_run <= now)
|
||||||
|
.filter(|(_, task)| {
|
||||||
|
// Check if task hasn't exceeded max runs
|
||||||
|
if let Some(max_runs) = task.max_runs {
|
||||||
|
task.run_count < max_runs
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|(id, _)| id.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for task_id in due_task_ids {
|
||||||
|
let execution = self.execute_task(&task_id, persona, transmission_controller).await?;
|
||||||
|
executions.push(execution);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_check = Some(now);
|
||||||
|
self.save_scheduler_data()?;
|
||||||
|
|
||||||
|
Ok(executions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_task(&mut self, task_id: &str, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<TaskExecution> {
|
||||||
|
let start_time = Utc::now();
|
||||||
|
let mut execution = TaskExecution {
|
||||||
|
task_id: task_id.to_string(),
|
||||||
|
execution_time: start_time,
|
||||||
|
duration_ms: 0,
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get task type without borrowing mutably
|
||||||
|
let task_type = {
|
||||||
|
let task = self.tasks.get(task_id)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Task not found: {}", task_id))?;
|
||||||
|
task.task_type.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the task based on its type
|
||||||
|
let result = match &task_type {
|
||||||
|
TaskType::DailyMaintenance => self.execute_daily_maintenance(persona, transmission_controller).await,
|
||||||
|
TaskType::AutoTransmission => self.execute_auto_transmission(persona, transmission_controller).await,
|
||||||
|
TaskType::RelationshipDecay => self.execute_relationship_decay(persona).await,
|
||||||
|
TaskType::BreakthroughCheck => self.execute_breakthrough_check(persona, transmission_controller).await,
|
||||||
|
TaskType::MaintenanceTransmission => self.execute_maintenance_transmission(persona, transmission_controller).await,
|
||||||
|
TaskType::Custom(name) => self.execute_custom_task(name, persona, transmission_controller).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_time = Utc::now();
|
||||||
|
execution.duration_ms = (end_time - start_time).num_milliseconds() as u64;
|
||||||
|
|
||||||
|
// Now update the task state with mutable borrow
|
||||||
|
match result {
|
||||||
|
Ok(message) => {
|
||||||
|
execution.success = true;
|
||||||
|
execution.result = Some(message);
|
||||||
|
|
||||||
|
// Update task state
|
||||||
|
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||||
|
task.last_run = Some(start_time);
|
||||||
|
task.run_count += 1;
|
||||||
|
|
||||||
|
// Schedule next run if recurring
|
||||||
|
if let Some(interval_hours) = task.interval_hours {
|
||||||
|
task.next_run = start_time + Duration::hours(interval_hours);
|
||||||
|
} else {
|
||||||
|
// One-time task, disable it
|
||||||
|
task.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
execution.error = Some(e.to_string());
|
||||||
|
|
||||||
|
// For failed tasks, retry in a shorter interval
|
||||||
|
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||||
|
if task.interval_hours.is_some() {
|
||||||
|
task.next_run = start_time + Duration::minutes(15); // Retry in 15 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.execution_history.push(execution.clone());
|
||||||
|
|
||||||
|
// Keep only recent execution history (last 1000 executions)
|
||||||
|
if self.execution_history.len() > 1000 {
|
||||||
|
self.execution_history.drain(..self.execution_history.len() - 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_daily_maintenance(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<String> {
|
||||||
|
// Run daily maintenance
|
||||||
|
persona.daily_maintenance()?;
|
||||||
|
|
||||||
|
// Check for maintenance transmissions
|
||||||
|
let transmissions = transmission_controller.check_maintenance_transmissions(persona).await?;
|
||||||
|
|
||||||
|
Ok(format!("Daily maintenance completed. {} maintenance transmissions sent.", transmissions.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_auto_transmission(&self, _persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<String> {
|
||||||
|
let transmissions = transmission_controller.check_autonomous_transmissions(_persona).await?;
|
||||||
|
Ok(format!("Autonomous transmission check completed. {} transmissions sent.", transmissions.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_relationship_decay(&self, persona: &mut Persona) -> Result<String> {
|
||||||
|
persona.daily_maintenance()?;
|
||||||
|
Ok("Relationship time decay applied.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_breakthrough_check(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<String> {
|
||||||
|
let transmissions = transmission_controller.check_breakthrough_transmissions(persona).await?;
|
||||||
|
Ok(format!("Breakthrough check completed. {} transmissions sent.", transmissions.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_maintenance_transmission(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result<String> {
|
||||||
|
let transmissions = transmission_controller.check_maintenance_transmissions(persona).await?;
|
||||||
|
Ok(format!("Maintenance transmission check completed. {} transmissions sent.", transmissions.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_custom_task(&self, _name: &str, _persona: &mut Persona, _transmission_controller: &mut TransmissionController) -> Result<String> {
|
||||||
|
// Placeholder for custom task execution
|
||||||
|
Ok("Custom task executed.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_task(&mut self, task_type: TaskType, next_run: DateTime<Utc>, interval_hours: Option<i64>) -> Result<String> {
|
||||||
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let task = ScheduledTask {
|
||||||
|
id: task_id.clone(),
|
||||||
|
task_type,
|
||||||
|
next_run,
|
||||||
|
interval_hours,
|
||||||
|
enabled: true,
|
||||||
|
last_run: None,
|
||||||
|
run_count: 0,
|
||||||
|
max_runs: None,
|
||||||
|
created_at: now,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tasks.insert(task_id.clone(), task);
|
||||||
|
self.save_scheduler_data()?;
|
||||||
|
|
||||||
|
Ok(task_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_task(&mut self, task_id: &str) -> Result<()> {
|
||||||
|
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||||
|
task.enabled = true;
|
||||||
|
self.save_scheduler_data()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_task(&mut self, task_id: &str) -> Result<()> {
|
||||||
|
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||||
|
task.enabled = false;
|
||||||
|
self.save_scheduler_data()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_task(&mut self, task_id: &str) -> Result<()> {
|
||||||
|
self.tasks.remove(task_id);
|
||||||
|
self.save_scheduler_data()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_task(&self, task_id: &str) -> Option<&ScheduledTask> {
|
||||||
|
self.tasks.get(task_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tasks(&self) -> &HashMap<String, ScheduledTask> {
|
||||||
|
&self.tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_due_tasks(&self) -> Vec<&ScheduledTask> {
|
||||||
|
let now = Utc::now();
|
||||||
|
self.tasks
|
||||||
|
.values()
|
||||||
|
.filter(|task| task.enabled && task.next_run <= now)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_execution_history(&self, limit: Option<usize>) -> Vec<&TaskExecution> {
|
||||||
|
let mut executions: Vec<_> = self.execution_history.iter().collect();
|
||||||
|
executions.sort_by(|a, b| b.execution_time.cmp(&a.execution_time));
|
||||||
|
|
||||||
|
match limit {
|
||||||
|
Some(limit) => executions.into_iter().take(limit).collect(),
|
||||||
|
None => executions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scheduler_stats(&self) -> SchedulerStats {
|
||||||
|
let total_tasks = self.tasks.len();
|
||||||
|
let enabled_tasks = self.tasks.values().filter(|task| task.enabled).count();
|
||||||
|
let due_tasks = self.get_due_tasks().len();
|
||||||
|
|
||||||
|
let total_executions = self.execution_history.len();
|
||||||
|
let successful_executions = self.execution_history.iter()
|
||||||
|
.filter(|exec| exec.success)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
let today_executions = self.execution_history.iter()
|
||||||
|
.filter(|exec| exec.execution_time.date_naive() == today)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let avg_duration = if total_executions > 0 {
|
||||||
|
self.execution_history.iter()
|
||||||
|
.map(|exec| exec.duration_ms)
|
||||||
|
.sum::<u64>() as f64 / total_executions as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
SchedulerStats {
|
||||||
|
total_tasks,
|
||||||
|
enabled_tasks,
|
||||||
|
due_tasks,
|
||||||
|
total_executions,
|
||||||
|
successful_executions,
|
||||||
|
today_executions,
|
||||||
|
success_rate: if total_executions > 0 {
|
||||||
|
successful_executions as f64 / total_executions as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
avg_duration_ms: avg_duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_default_tasks(&mut self) -> Result<()> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Daily maintenance task - run every day at 3 AM
|
||||||
|
let mut daily_maintenance_time = now.date_naive().and_hms_opt(3, 0, 0).unwrap().and_utc();
|
||||||
|
if daily_maintenance_time <= now {
|
||||||
|
daily_maintenance_time = daily_maintenance_time + Duration::days(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.create_task(
|
||||||
|
TaskType::DailyMaintenance,
|
||||||
|
daily_maintenance_time,
|
||||||
|
Some(24), // 24 hours = 1 day
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Auto transmission check - every 4 hours
|
||||||
|
self.create_task(
|
||||||
|
TaskType::AutoTransmission,
|
||||||
|
now + Duration::hours(1),
|
||||||
|
Some(4),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Breakthrough check - every 2 hours
|
||||||
|
self.create_task(
|
||||||
|
TaskType::BreakthroughCheck,
|
||||||
|
now + Duration::minutes(30),
|
||||||
|
Some(2),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Maintenance transmission - once per day
|
||||||
|
let mut maintenance_time = now.date_naive().and_hms_opt(12, 0, 0).unwrap().and_utc();
|
||||||
|
if maintenance_time <= now {
|
||||||
|
maintenance_time = maintenance_time + Duration::days(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.create_task(
|
||||||
|
TaskType::MaintenanceTransmission,
|
||||||
|
maintenance_time,
|
||||||
|
Some(24), // 24 hours = 1 day
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_scheduler_data(config: &Config) -> Result<(HashMap<String, ScheduledTask>, Vec<TaskExecution>)> {
|
||||||
|
let tasks_file = config.scheduler_tasks_file();
|
||||||
|
let history_file = config.scheduler_history_file();
|
||||||
|
|
||||||
|
let tasks = if tasks_file.exists() {
|
||||||
|
let content = std::fs::read_to_string(tasks_file)
|
||||||
|
.context("Failed to read scheduler tasks file")?;
|
||||||
|
serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse scheduler tasks file")?
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let history = if history_file.exists() {
|
||||||
|
let content = std::fs::read_to_string(history_file)
|
||||||
|
.context("Failed to read scheduler history file")?;
|
||||||
|
serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse scheduler history file")?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((tasks, history))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_scheduler_data(&self) -> Result<()> {
|
||||||
|
// Save tasks
|
||||||
|
let tasks_content = serde_json::to_string_pretty(&self.tasks)
|
||||||
|
.context("Failed to serialize scheduler tasks")?;
|
||||||
|
std::fs::write(&self.config.scheduler_tasks_file(), tasks_content)
|
||||||
|
.context("Failed to write scheduler tasks file")?;
|
||||||
|
|
||||||
|
// Save execution history
|
||||||
|
let history_content = serde_json::to_string_pretty(&self.execution_history)
|
||||||
|
.context("Failed to serialize scheduler history")?;
|
||||||
|
std::fs::write(&self.config.scheduler_history_file(), history_content)
|
||||||
|
.context("Failed to write scheduler history file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type alias for compatibility with CLI interface
|
||||||
|
pub type Scheduler = AIScheduler;
|
||||||
|
|
||||||
|
impl Scheduler {
|
||||||
|
pub fn list_tasks(&self) -> Result<Vec<ScheduledTaskInfo>> {
|
||||||
|
let tasks: Vec<ScheduledTaskInfo> = self.tasks
|
||||||
|
.values()
|
||||||
|
.map(|task| ScheduledTaskInfo {
|
||||||
|
name: task.task_type.to_string(),
|
||||||
|
schedule: match task.interval_hours {
|
||||||
|
Some(hours) => format!("Every {} hours", hours),
|
||||||
|
None => "One-time".to_string(),
|
||||||
|
},
|
||||||
|
next_run: task.next_run,
|
||||||
|
enabled: task.enabled,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SchedulerStats {
|
||||||
|
pub total_tasks: usize,
|
||||||
|
pub enabled_tasks: usize,
|
||||||
|
pub due_tasks: usize,
|
||||||
|
pub total_executions: usize,
|
||||||
|
pub successful_executions: usize,
|
||||||
|
pub today_executions: usize,
|
||||||
|
pub success_rate: f64,
|
||||||
|
pub avg_duration_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScheduledTaskInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub schedule: String,
|
||||||
|
pub next_run: DateTime<Utc>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
608
src/shell.rs
Normal file
608
src/shell.rs
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use colored::*;
|
||||||
|
use rustyline::error::ReadlineError;
|
||||||
|
use rustyline::Editor;
|
||||||
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
|
use rustyline::history::{History, DefaultHistory};
|
||||||
|
use rustyline::highlight::Highlighter;
|
||||||
|
use rustyline::hint::Hinter;
|
||||||
|
use rustyline::validate::Validator;
|
||||||
|
use rustyline::Helper;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::ai_provider::{AIProviderClient, AIProvider, AIConfig};
|
||||||
|
|
||||||
|
pub async fn handle_shell(
|
||||||
|
user_id: String,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
model: Option<String>,
|
||||||
|
provider: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
|
||||||
|
let mut shell = ShellMode::new(config, user_id)?
|
||||||
|
.with_ai_provider(provider, model);
|
||||||
|
|
||||||
|
shell.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ShellMode {
|
||||||
|
config: Config,
|
||||||
|
persona: Persona,
|
||||||
|
ai_provider: Option<AIProviderClient>,
|
||||||
|
user_id: String,
|
||||||
|
editor: Editor<ShellCompleter, DefaultHistory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellCompleter {
|
||||||
|
completer: FilenameCompleter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellCompleter {
|
||||||
|
fn new() -> Self {
|
||||||
|
ShellCompleter {
|
||||||
|
completer: FilenameCompleter::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Helper for ShellCompleter {}
|
||||||
|
|
||||||
|
impl Hinter for ShellCompleter {
|
||||||
|
type Hint = String;
|
||||||
|
|
||||||
|
fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Highlighter for ShellCompleter {}
|
||||||
|
|
||||||
|
impl Validator for ShellCompleter {}
|
||||||
|
|
||||||
|
impl Completer for ShellCompleter {
|
||||||
|
type Candidate = Pair;
|
||||||
|
|
||||||
|
fn complete(
|
||||||
|
&self,
|
||||||
|
line: &str,
|
||||||
|
pos: usize,
|
||||||
|
ctx: &rustyline::Context<'_>,
|
||||||
|
) -> rustyline::Result<(usize, Vec<Pair>)> {
|
||||||
|
// Custom completion for slash commands
|
||||||
|
if line.starts_with('/') {
|
||||||
|
let commands = vec![
|
||||||
|
"/status", "/relationships", "/memories", "/analyze",
|
||||||
|
"/fortune", "/clear", "/history", "/help", "/exit"
|
||||||
|
];
|
||||||
|
|
||||||
|
let word_start = line.rfind(' ').map_or(0, |i| i + 1);
|
||||||
|
let word = &line[word_start..pos];
|
||||||
|
|
||||||
|
let matches: Vec<Pair> = commands.iter()
|
||||||
|
.filter(|cmd| cmd.starts_with(word))
|
||||||
|
.map(|cmd| Pair {
|
||||||
|
display: cmd.to_string(),
|
||||||
|
replacement: cmd.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Ok((word_start, matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom completion for shell commands starting with !
|
||||||
|
if line.starts_with('!') {
|
||||||
|
let shell_commands = vec![
|
||||||
|
"ls", "pwd", "cd", "cat", "grep", "find", "ps", "top",
|
||||||
|
"echo", "mkdir", "rmdir", "cp", "mv", "rm", "touch",
|
||||||
|
"git", "cargo", "npm", "python", "node"
|
||||||
|
];
|
||||||
|
|
||||||
|
let word_start = line.rfind(' ').map_or(1, |i| i + 1); // Skip the '!'
|
||||||
|
let word = &line[word_start..pos];
|
||||||
|
|
||||||
|
let matches: Vec<Pair> = shell_commands.iter()
|
||||||
|
.filter(|cmd| cmd.starts_with(word))
|
||||||
|
.map(|cmd| Pair {
|
||||||
|
display: cmd.to_string(),
|
||||||
|
replacement: cmd.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Ok((word_start, matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to filename completion
|
||||||
|
self.completer.complete(line, pos, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellMode {
|
||||||
|
pub fn new(config: Config, user_id: String) -> Result<Self> {
|
||||||
|
let persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
// Setup rustyline editor with completer
|
||||||
|
let completer = ShellCompleter::new();
|
||||||
|
let mut editor = Editor::with_config(
|
||||||
|
rustyline::Config::builder()
|
||||||
|
.tab_stop(4)
|
||||||
|
.build()
|
||||||
|
)?;
|
||||||
|
editor.set_helper(Some(completer));
|
||||||
|
|
||||||
|
// Load history if exists
|
||||||
|
let history_file = config.data_dir.join("shell_history.txt");
|
||||||
|
if history_file.exists() {
|
||||||
|
let _ = editor.load_history(&history_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ShellMode {
|
||||||
|
config,
|
||||||
|
persona,
|
||||||
|
ai_provider: None,
|
||||||
|
user_id,
|
||||||
|
editor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_ai_provider(mut self, provider: Option<String>, model: Option<String>) -> Self {
|
||||||
|
// Use provided parameters or fall back to config defaults
|
||||||
|
let provider_name = provider
|
||||||
|
.or_else(|| Some(self.config.default_provider.clone()))
|
||||||
|
.unwrap_or_else(|| "ollama".to_string());
|
||||||
|
|
||||||
|
let model_name = model.or_else(|| {
|
||||||
|
// Try to get default model from config for the chosen provider
|
||||||
|
self.config.providers.get(&provider_name)
|
||||||
|
.map(|p| p.default_model.clone())
|
||||||
|
}).unwrap_or_else(|| {
|
||||||
|
// Final fallback based on provider
|
||||||
|
match provider_name.as_str() {
|
||||||
|
"openai" => "gpt-4o-mini".to_string(),
|
||||||
|
"ollama" => "qwen2.5-coder:latest".to_string(),
|
||||||
|
_ => "qwen2.5-coder:latest".to_string(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ai_provider = match provider_name.as_str() {
|
||||||
|
"ollama" => AIProvider::Ollama,
|
||||||
|
"openai" => AIProvider::OpenAI,
|
||||||
|
"claude" => AIProvider::Claude,
|
||||||
|
_ => AIProvider::Ollama, // Default fallback
|
||||||
|
};
|
||||||
|
|
||||||
|
let ai_config = AIConfig {
|
||||||
|
provider: ai_provider,
|
||||||
|
model: model_name,
|
||||||
|
api_key: None, // Will be loaded from environment if needed
|
||||||
|
base_url: None,
|
||||||
|
max_tokens: Some(2000),
|
||||||
|
temperature: Some(0.7),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = AIProviderClient::new(ai_config);
|
||||||
|
self.ai_provider = Some(client);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
println!("{}", "🚀 Starting ai.gpt Interactive Shell".cyan().bold());
|
||||||
|
|
||||||
|
// Show AI provider info
|
||||||
|
if let Some(ai_provider) = &self.ai_provider {
|
||||||
|
println!("{}: {} ({})",
|
||||||
|
"AI Provider".green().bold(),
|
||||||
|
ai_provider.get_provider().to_string(),
|
||||||
|
ai_provider.get_model());
|
||||||
|
} else {
|
||||||
|
println!("{}: {}", "AI Provider".yellow().bold(), "Simple mode (no AI)");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "Type 'help' for commands, 'exit' to quit".dimmed());
|
||||||
|
println!("{}", "Use Tab for command completion, Ctrl+C to interrupt, Ctrl+D to exit".dimmed());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read user input with rustyline (supports completion, history, etc.)
|
||||||
|
let readline = self.editor.readline("ai.shell> ");
|
||||||
|
|
||||||
|
match readline {
|
||||||
|
Ok(line) => {
|
||||||
|
let input = line.trim();
|
||||||
|
|
||||||
|
// Skip empty input
|
||||||
|
if input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
self.editor.add_history_entry(input)
|
||||||
|
.context("Failed to add to history")?;
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
if let Err(e) = self.handle_input(input).await {
|
||||||
|
println!("{}: {}", "Error".red().bold(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Interrupted) => {
|
||||||
|
// Ctrl+C
|
||||||
|
println!("{}", "Use 'exit' or Ctrl+D to quit".yellow());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Eof) => {
|
||||||
|
// Ctrl+D
|
||||||
|
println!("\n{}", "Goodbye!".cyan());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("{}: {}", "Input error".red().bold(), err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save history before exit
|
||||||
|
self.save_history()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_input(&mut self, input: &str) -> Result<()> {
|
||||||
|
match input {
|
||||||
|
// Exit commands
|
||||||
|
"exit" | "quit" | "/exit" | "/quit" => {
|
||||||
|
println!("{}", "Goodbye!".cyan());
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
// Help command
|
||||||
|
"help" | "/help" => {
|
||||||
|
self.show_help();
|
||||||
|
}
|
||||||
|
// Shell commands (starting with !)
|
||||||
|
input if input.starts_with('!') => {
|
||||||
|
self.execute_shell_command(&input[1..]).await?;
|
||||||
|
}
|
||||||
|
// Slash commands (starting with /)
|
||||||
|
input if input.starts_with('/') => {
|
||||||
|
self.execute_slash_command(input).await?;
|
||||||
|
}
|
||||||
|
// AI conversation
|
||||||
|
_ => {
|
||||||
|
self.handle_ai_conversation(input).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_help(&self) {
|
||||||
|
println!("\n{}", "ai.gpt Interactive Shell Commands".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("{}", "Navigation & Input:".yellow().bold());
|
||||||
|
println!(" {} - Tab completion for commands and files", "Tab".green());
|
||||||
|
println!(" {} - Command history (previous/next)", "↑/↓ or Ctrl+P/N".green());
|
||||||
|
println!(" {} - Interrupt current input", "Ctrl+C".green());
|
||||||
|
println!(" {} - Exit shell", "Ctrl+D".green());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("{}", "Basic Commands:".yellow().bold());
|
||||||
|
println!(" {} - Show this help", "help".green());
|
||||||
|
println!(" {} - Exit the shell", "exit, quit".green());
|
||||||
|
println!(" {} - Clear screen", "/clear".green());
|
||||||
|
println!(" {} - Show command history", "/history".green());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("{}", "Shell Commands:".yellow().bold());
|
||||||
|
println!(" {} - Execute shell command (Tab completion)", "!<command>".green());
|
||||||
|
println!(" {} - List files", "!ls".green());
|
||||||
|
println!(" {} - Show current directory", "!pwd".green());
|
||||||
|
println!(" {} - Git status", "!git status".green());
|
||||||
|
println!(" {} - Cargo build", "!cargo build".green());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("{}", "AI Commands:".yellow().bold());
|
||||||
|
println!(" {} - Show AI status and relationship", "/status".green());
|
||||||
|
println!(" {} - List all relationships", "/relationships".green());
|
||||||
|
println!(" {} - Show recent memories", "/memories".green());
|
||||||
|
println!(" {} - Analyze current directory", "/analyze".green());
|
||||||
|
println!(" {} - Show today's fortune", "/fortune".green());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("{}", "Conversation:".yellow().bold());
|
||||||
|
println!(" {} - Chat with AI using configured provider", "Any other input".green());
|
||||||
|
println!(" {} - AI responses track relationship changes", "Relationship tracking".dimmed());
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_shell_command(&self, command: &str) -> Result<()> {
|
||||||
|
println!("{} {}", "Executing:".blue().bold(), command.yellow());
|
||||||
|
|
||||||
|
let output = if cfg!(target_os = "windows") {
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(["/C", command])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute command")?
|
||||||
|
} else {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", command])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute command")?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Print stdout
|
||||||
|
if !output.stdout.is_empty() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("{}", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print stderr in red
|
||||||
|
if !output.stderr.is_empty() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
println!("{}", stderr.red());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show exit code if not successful
|
||||||
|
if !output.status.success() {
|
||||||
|
if let Some(code) = output.status.code() {
|
||||||
|
println!("{}: {}", "Exit code".red().bold(), code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_slash_command(&mut self, command: &str) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
"/status" => {
|
||||||
|
self.show_ai_status().await?;
|
||||||
|
}
|
||||||
|
"/relationships" => {
|
||||||
|
self.show_relationships().await?;
|
||||||
|
}
|
||||||
|
"/memories" => {
|
||||||
|
self.show_memories().await?;
|
||||||
|
}
|
||||||
|
"/analyze" => {
|
||||||
|
self.analyze_directory().await?;
|
||||||
|
}
|
||||||
|
"/fortune" => {
|
||||||
|
self.show_fortune().await?;
|
||||||
|
}
|
||||||
|
"/clear" => {
|
||||||
|
// Clear screen
|
||||||
|
print!("\x1B[2J\x1B[1;1H");
|
||||||
|
io::stdout().flush()?;
|
||||||
|
}
|
||||||
|
"/history" => {
|
||||||
|
self.show_history();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("{}: {}", "Unknown command".red().bold(), command);
|
||||||
|
println!("Type '{}' for available commands", "help".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_ai_conversation(&mut self, input: &str) -> Result<()> {
|
||||||
|
let (response, relationship_delta) = if let Some(ai_provider) = &self.ai_provider {
|
||||||
|
// Use AI provider for response
|
||||||
|
self.persona.process_ai_interaction(&self.user_id, input,
|
||||||
|
Some(ai_provider.get_provider().to_string()),
|
||||||
|
Some(ai_provider.get_model().to_string())).await?
|
||||||
|
} else {
|
||||||
|
// Use simple response
|
||||||
|
self.persona.process_interaction(&self.user_id, input)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display conversation
|
||||||
|
println!("{}: {}", "You".cyan().bold(), input);
|
||||||
|
println!("{}: {}", "AI".green().bold(), response);
|
||||||
|
|
||||||
|
// Show relationship change if significant
|
||||||
|
if relationship_delta.abs() >= 0.1 {
|
||||||
|
if relationship_delta > 0.0 {
|
||||||
|
println!("{}", format!("(+{:.2} relationship)", relationship_delta).green());
|
||||||
|
} else {
|
||||||
|
println!("{}", format!("({:.2} relationship)", relationship_delta).red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(); // Add spacing
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_ai_status(&self) -> Result<()> {
|
||||||
|
let state = self.persona.get_current_state()?;
|
||||||
|
|
||||||
|
println!("\n{}", "AI Status".cyan().bold());
|
||||||
|
println!("Mood: {}", state.current_mood.yellow());
|
||||||
|
println!("Fortune: {}/10", state.fortune_value.to_string().yellow());
|
||||||
|
|
||||||
|
if let Some(relationship) = self.persona.get_relationship(&self.user_id) {
|
||||||
|
println!("\n{}", "Your Relationship".cyan().bold());
|
||||||
|
println!("Status: {}", relationship.status.to_string().yellow());
|
||||||
|
println!("Score: {:.2} / {}", relationship.score, relationship.threshold);
|
||||||
|
println!("Interactions: {}", relationship.total_interactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_relationships(&self) -> Result<()> {
|
||||||
|
let relationships = self.persona.list_all_relationships();
|
||||||
|
|
||||||
|
if relationships.is_empty() {
|
||||||
|
println!("{}", "No relationships yet".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n{}", "All Relationships".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for (user_id, rel) in relationships {
|
||||||
|
let transmission = if rel.is_broken {
|
||||||
|
"💔"
|
||||||
|
} else if rel.transmission_enabled {
|
||||||
|
"✓"
|
||||||
|
} else {
|
||||||
|
"✗"
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_display = if user_id.len() > 20 {
|
||||||
|
format!("{}...", &user_id[..20])
|
||||||
|
} else {
|
||||||
|
user_id
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:<25} {:<12} {:<8} {}",
|
||||||
|
user_display.cyan(),
|
||||||
|
rel.status.to_string(),
|
||||||
|
format!("{:.2}", rel.score),
|
||||||
|
transmission);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_memories(&mut self) -> Result<()> {
|
||||||
|
let memories = self.persona.get_memories(&self.user_id, 10);
|
||||||
|
|
||||||
|
if memories.is_empty() {
|
||||||
|
println!("{}", "No memories yet".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n{}", "Recent Memories".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for (i, memory) in memories.iter().enumerate() {
|
||||||
|
println!("{}: {}",
|
||||||
|
format!("Memory {}", i + 1).dimmed(),
|
||||||
|
memory);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn analyze_directory(&self) -> Result<()> {
|
||||||
|
println!("{}", "Analyzing current directory...".blue().bold());
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
let current_dir = std::env::current_dir()
|
||||||
|
.context("Failed to get current directory")?;
|
||||||
|
|
||||||
|
println!("Directory: {}", current_dir.display().to_string().yellow());
|
||||||
|
|
||||||
|
// List files and directories
|
||||||
|
let entries = std::fs::read_dir(¤t_dir)
|
||||||
|
.context("Failed to read directory")?;
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.context("Failed to read directory entry")?;
|
||||||
|
let path = entry.path();
|
||||||
|
let name = path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
dirs.push(name.to_string());
|
||||||
|
} else {
|
||||||
|
files.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dirs.is_empty() {
|
||||||
|
println!("\n{}: {}", "Directories".blue().bold(), dirs.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !files.is_empty() {
|
||||||
|
println!("{}: {}", "Files".blue().bold(), files.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common project files
|
||||||
|
let project_files = ["Cargo.toml", "package.json", "requirements.txt", "Makefile", "README.md"];
|
||||||
|
let found_files: Vec<_> = project_files.iter()
|
||||||
|
.filter(|&&file| files.contains(&file.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !found_files.is_empty() {
|
||||||
|
println!("\n{}: {}", "Project files detected".green().bold(),
|
||||||
|
found_files.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_fortune(&self) -> Result<()> {
|
||||||
|
let state = self.persona.get_current_state()?;
|
||||||
|
|
||||||
|
let fortune_stars = "🌟".repeat(state.fortune_value as usize);
|
||||||
|
let empty_stars = "☆".repeat((10 - state.fortune_value) as usize);
|
||||||
|
|
||||||
|
println!("\n{}", "AI Fortune".yellow().bold());
|
||||||
|
println!("{}{}", fortune_stars, empty_stars);
|
||||||
|
println!("Today's Fortune: {}/10", state.fortune_value);
|
||||||
|
|
||||||
|
if state.breakthrough_triggered {
|
||||||
|
println!("{}", "⚡ BREAKTHROUGH! Special fortune activated!".yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_history(&self) {
|
||||||
|
println!("\n{}", "Command History".cyan().bold());
|
||||||
|
|
||||||
|
let history = self.editor.history();
|
||||||
|
if history.is_empty() {
|
||||||
|
println!("{}", "No commands in history".yellow());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show last 20 commands
|
||||||
|
let start = if history.len() > 20 { history.len() - 20 } else { 0 };
|
||||||
|
for (i, entry) in history.iter().enumerate().skip(start) {
|
||||||
|
println!("{:2}: {}", i + 1, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_history(&mut self) -> Result<()> {
|
||||||
|
let history_file = self.config.data_dir.join("shell_history.txt");
|
||||||
|
self.editor.save_history(&history_file)
|
||||||
|
.context("Failed to save shell history")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend AIProvider to have Display and helper methods
|
||||||
|
impl AIProvider {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AIProvider::OpenAI => "openai".to_string(),
|
||||||
|
AIProvider::Ollama => "ollama".to_string(),
|
||||||
|
AIProvider::Claude => "claude".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/status.rs
Normal file
51
src/status.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::*;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
|
||||||
|
pub async fn handle_status(user_id: Option<String>, data_dir: Option<PathBuf>) -> Result<()> {
|
||||||
|
// Load configuration
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
|
||||||
|
// Initialize persona
|
||||||
|
let persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
let state = persona.get_current_state()?;
|
||||||
|
|
||||||
|
// Display AI status
|
||||||
|
println!("{}", "ai.gpt Status".cyan().bold());
|
||||||
|
println!("Mood: {}", state.current_mood);
|
||||||
|
println!("Fortune: {}/10", state.fortune_value);
|
||||||
|
|
||||||
|
if state.breakthrough_triggered {
|
||||||
|
println!("{}", "⚡ Breakthrough triggered!".yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show personality traits
|
||||||
|
println!("\n{}", "Current Personality".cyan().bold());
|
||||||
|
for (trait_name, value) in &state.base_personality {
|
||||||
|
println!("{}: {:.2}", trait_name.cyan(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show specific relationship if requested
|
||||||
|
if let Some(user_id) = user_id {
|
||||||
|
if let Some(relationship) = persona.get_relationship(&user_id) {
|
||||||
|
println!("\n{}: {}", "Relationship with".cyan(), user_id);
|
||||||
|
println!("Status: {}", relationship.status);
|
||||||
|
println!("Score: {:.2}", relationship.score);
|
||||||
|
println!("Total Interactions: {}", relationship.total_interactions);
|
||||||
|
println!("Transmission Enabled: {}", relationship.transmission_enabled);
|
||||||
|
|
||||||
|
if relationship.is_broken {
|
||||||
|
println!("{}", "⚠️ This relationship is broken and cannot be repaired.".red());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("\n{}: {}", "No relationship found with".yellow(), user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
480
src/submodules.rs
Normal file
480
src/submodules.rs
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use colored::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
pub async fn handle_submodules(
|
||||||
|
action: String,
|
||||||
|
module: Option<String>,
|
||||||
|
all: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
auto_commit: bool,
|
||||||
|
verbose: bool,
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Config::new(data_dir)?;
|
||||||
|
let mut submodule_manager = SubmoduleManager::new(config);
|
||||||
|
|
||||||
|
match action.as_str() {
|
||||||
|
"list" => {
|
||||||
|
submodule_manager.list_submodules(verbose).await?;
|
||||||
|
}
|
||||||
|
"update" => {
|
||||||
|
submodule_manager.update_submodules(module, all, dry_run, auto_commit, verbose).await?;
|
||||||
|
}
|
||||||
|
"status" => {
|
||||||
|
submodule_manager.show_submodule_status().await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!("Unknown submodule action: {}", action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubmoduleInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub branch: String,
|
||||||
|
pub current_commit: Option<String>,
|
||||||
|
pub target_commit: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SubmoduleInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
SubmoduleInfo {
|
||||||
|
name: String::new(),
|
||||||
|
path: String::new(),
|
||||||
|
branch: "main".to_string(),
|
||||||
|
current_commit: None,
|
||||||
|
target_commit: None,
|
||||||
|
status: "unknown".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct SubmoduleManager {
|
||||||
|
config: Config,
|
||||||
|
ai_root: PathBuf,
|
||||||
|
submodules: HashMap<String, SubmoduleInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubmoduleManager {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let ai_root = dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join("ai")
|
||||||
|
.join("ai");
|
||||||
|
|
||||||
|
SubmoduleManager {
|
||||||
|
config,
|
||||||
|
ai_root,
|
||||||
|
submodules: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_submodules(&mut self, verbose: bool) -> Result<()> {
|
||||||
|
println!("{}", "📋 Submodules Status".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let submodules = self.parse_gitmodules()?;
|
||||||
|
|
||||||
|
if submodules.is_empty() {
|
||||||
|
println!("{}", "No submodules found".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display submodules in a table format
|
||||||
|
println!("{:<15} {:<25} {:<15} {}",
|
||||||
|
"Module".cyan().bold(),
|
||||||
|
"Path".cyan().bold(),
|
||||||
|
"Branch".cyan().bold(),
|
||||||
|
"Status".cyan().bold());
|
||||||
|
println!("{}", "-".repeat(80));
|
||||||
|
|
||||||
|
for (module_name, module_info) in &submodules {
|
||||||
|
let status_color = match module_info.status.as_str() {
|
||||||
|
"clean" => module_info.status.green(),
|
||||||
|
"modified" => module_info.status.yellow(),
|
||||||
|
"missing" => module_info.status.red(),
|
||||||
|
"conflicts" => module_info.status.red(),
|
||||||
|
_ => module_info.status.normal(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:<15} {:<25} {:<15} {}",
|
||||||
|
module_name.blue(),
|
||||||
|
module_info.path,
|
||||||
|
module_info.branch.green(),
|
||||||
|
status_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("Total submodules: {}", submodules.len().to_string().cyan());
|
||||||
|
println!("Repository root: {}", self.ai_root.display().to_string().blue());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_submodules(
|
||||||
|
&mut self,
|
||||||
|
module: Option<String>,
|
||||||
|
all: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
auto_commit: bool,
|
||||||
|
verbose: bool
|
||||||
|
) -> Result<()> {
|
||||||
|
if !module.is_some() && !all {
|
||||||
|
return Err(anyhow::anyhow!("Either --module or --all is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if module.is_some() && all {
|
||||||
|
return Err(anyhow::anyhow!("Cannot use both --module and --all"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let submodules = self.parse_gitmodules()?;
|
||||||
|
|
||||||
|
if submodules.is_empty() {
|
||||||
|
println!("{}", "No submodules found".yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which modules to update
|
||||||
|
let modules_to_update: Vec<String> = if all {
|
||||||
|
submodules.keys().cloned().collect()
|
||||||
|
} else if let Some(module_name) = module {
|
||||||
|
if !submodules.contains_key(&module_name) {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Submodule '{}' not found. Available modules: {}",
|
||||||
|
module_name,
|
||||||
|
submodules.keys().cloned().collect::<Vec<_>>().join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
vec![module_name]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
println!("{}", "🔍 DRY RUN MODE - No changes will be made".yellow().bold());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", format!("🔄 Updating {} submodule(s)...", modules_to_update.len()).cyan().bold());
|
||||||
|
|
||||||
|
let mut updated_modules = Vec::new();
|
||||||
|
|
||||||
|
for module_name in modules_to_update {
|
||||||
|
if let Some(module_info) = submodules.get(&module_name) {
|
||||||
|
println!("\n{}", format!("📦 Processing: {}", module_name).blue().bold());
|
||||||
|
|
||||||
|
let module_path = PathBuf::from(&module_info.path);
|
||||||
|
let full_path = self.ai_root.join(&module_path);
|
||||||
|
|
||||||
|
if !full_path.exists() {
|
||||||
|
println!("{}", format!("❌ Module directory not found: {}", module_info.path).red());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current commit
|
||||||
|
let current_commit = self.get_current_commit(&full_path)?;
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
println!("{}", format!("🔍 Would update {} to branch {}", module_name, module_info.branch).yellow());
|
||||||
|
if let Some(ref commit) = current_commit {
|
||||||
|
println!("{}", format!("Current: {}", commit).dimmed());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform update
|
||||||
|
if let Err(e) = self.update_single_module(&module_name, &module_info, &full_path).await {
|
||||||
|
println!("{}", format!("❌ Failed to update {}: {}", module_name, e).red());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new commit
|
||||||
|
let new_commit = self.get_current_commit(&full_path)?;
|
||||||
|
|
||||||
|
if current_commit != new_commit {
|
||||||
|
println!("{}", format!("✅ Updated {} ({:?} → {:?})",
|
||||||
|
module_name,
|
||||||
|
current_commit.as_deref().unwrap_or("unknown"),
|
||||||
|
new_commit.as_deref().unwrap_or("unknown")).green());
|
||||||
|
updated_modules.push((module_name.clone(), current_commit, new_commit));
|
||||||
|
} else {
|
||||||
|
println!("{}", "✅ Already up to date".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
if !updated_modules.is_empty() {
|
||||||
|
println!("\n{}", format!("🎉 Successfully updated {} module(s)", updated_modules.len()).green().bold());
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
for (module_name, old_commit, new_commit) in &updated_modules {
|
||||||
|
println!(" • {}: {:?} → {:?}",
|
||||||
|
module_name,
|
||||||
|
old_commit.as_deref().unwrap_or("unknown"),
|
||||||
|
new_commit.as_deref().unwrap_or("unknown"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if auto_commit && !dry_run {
|
||||||
|
self.auto_commit_changes(&updated_modules).await?;
|
||||||
|
} else if !dry_run {
|
||||||
|
println!("{}", "💾 Changes staged but not committed".yellow());
|
||||||
|
println!("Run with --auto-commit to commit automatically");
|
||||||
|
}
|
||||||
|
} else if !dry_run {
|
||||||
|
println!("{}", "No modules needed updating".yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_submodule_status(&self) -> Result<()> {
|
||||||
|
println!("{}", "📊 Submodule Status Overview".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let submodules = self.parse_gitmodules()?;
|
||||||
|
let mut total_modules = 0;
|
||||||
|
let mut clean_modules = 0;
|
||||||
|
let mut modified_modules = 0;
|
||||||
|
let mut missing_modules = 0;
|
||||||
|
|
||||||
|
for (module_name, module_info) in submodules {
|
||||||
|
let module_path = self.ai_root.join(&module_info.path);
|
||||||
|
|
||||||
|
if module_path.exists() {
|
||||||
|
total_modules += 1;
|
||||||
|
match module_info.status.as_str() {
|
||||||
|
"clean" => clean_modules += 1,
|
||||||
|
"modified" => modified_modules += 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing_modules += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}: {}",
|
||||||
|
module_name.blue(),
|
||||||
|
if module_path.exists() {
|
||||||
|
module_info.status.green()
|
||||||
|
} else {
|
||||||
|
"missing".red()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("Summary: {} total, {} clean, {} modified, {} missing",
|
||||||
|
total_modules.to_string().cyan(),
|
||||||
|
clean_modules.to_string().green(),
|
||||||
|
modified_modules.to_string().yellow(),
|
||||||
|
missing_modules.to_string().red());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gitmodules(&self) -> Result<HashMap<String, SubmoduleInfo>> {
|
||||||
|
let gitmodules_path = self.ai_root.join(".gitmodules");
|
||||||
|
|
||||||
|
if !gitmodules_path.exists() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&gitmodules_path)
|
||||||
|
.with_context(|| format!("Failed to read .gitmodules file: {}", gitmodules_path.display()))?;
|
||||||
|
|
||||||
|
let mut submodules = HashMap::new();
|
||||||
|
let mut current_name: Option<String> = None;
|
||||||
|
let mut current_path: Option<String> = None;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
if line.starts_with("[submodule \"") && line.ends_with("\"]") {
|
||||||
|
// Save previous submodule if complete
|
||||||
|
if let (Some(name), Some(path)) = (current_name.take(), current_path.take()) {
|
||||||
|
let mut info = SubmoduleInfo::default();
|
||||||
|
info.name = name.clone();
|
||||||
|
info.path = path;
|
||||||
|
info.branch = self.get_target_branch(&name);
|
||||||
|
info.status = self.get_submodule_status(&name, &info.path)?;
|
||||||
|
submodules.insert(name, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract new submodule name
|
||||||
|
current_name = Some(line[12..line.len()-2].to_string());
|
||||||
|
} else if line.starts_with("path = ") {
|
||||||
|
current_path = Some(line[7..].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save last submodule
|
||||||
|
if let (Some(name), Some(path)) = (current_name, current_path) {
|
||||||
|
let mut info = SubmoduleInfo::default();
|
||||||
|
info.name = name.clone();
|
||||||
|
info.path = path;
|
||||||
|
info.branch = self.get_target_branch(&name);
|
||||||
|
info.status = self.get_submodule_status(&name, &info.path)?;
|
||||||
|
submodules.insert(name, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(submodules)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_target_branch(&self, module_name: &str) -> String {
|
||||||
|
// Try to get from ai.json configuration
|
||||||
|
match module_name {
|
||||||
|
"verse" => "main".to_string(),
|
||||||
|
"card" => "main".to_string(),
|
||||||
|
"bot" => "main".to_string(),
|
||||||
|
_ => "main".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_submodule_status(&self, _module_name: &str, module_path: &str) -> Result<String> {
|
||||||
|
let full_path = self.ai_root.join(module_path);
|
||||||
|
|
||||||
|
if !full_path.exists() {
|
||||||
|
return Ok("missing".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check git status
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(&["submodule", "status", module_path])
|
||||||
|
.current_dir(&self.ai_root)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if let Some(status_char) = stdout.chars().next() {
|
||||||
|
match status_char {
|
||||||
|
' ' => Ok("clean".to_string()),
|
||||||
|
'+' => Ok("modified".to_string()),
|
||||||
|
'-' => Ok("not_initialized".to_string()),
|
||||||
|
'U' => Ok("conflicts".to_string()),
|
||||||
|
_ => Ok("unknown".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok("unknown".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok("unknown".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_commit(&self, module_path: &PathBuf) -> Result<Option<String>> {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(&["rev-parse", "HEAD"])
|
||||||
|
.current_dir(module_path)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if commit.len() >= 8 {
|
||||||
|
Ok(Some(commit[..8].to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Some(commit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_single_module(
|
||||||
|
&self,
|
||||||
|
_module_name: &str,
|
||||||
|
module_info: &SubmoduleInfo,
|
||||||
|
module_path: &PathBuf
|
||||||
|
) -> Result<()> {
|
||||||
|
// Fetch latest changes
|
||||||
|
println!("{}", "Fetching latest changes...".dimmed());
|
||||||
|
let fetch_output = std::process::Command::new("git")
|
||||||
|
.args(&["fetch", "origin"])
|
||||||
|
.current_dir(module_path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !fetch_output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to fetch: {}",
|
||||||
|
String::from_utf8_lossy(&fetch_output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to target branch
|
||||||
|
println!("{}", format!("Switching to branch {}...", module_info.branch).dimmed());
|
||||||
|
let checkout_output = std::process::Command::new("git")
|
||||||
|
.args(&["checkout", &module_info.branch])
|
||||||
|
.current_dir(module_path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !checkout_output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to checkout {}: {}",
|
||||||
|
module_info.branch, String::from_utf8_lossy(&checkout_output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull latest changes
|
||||||
|
let pull_output = std::process::Command::new("git")
|
||||||
|
.args(&["pull", "origin", &module_info.branch])
|
||||||
|
.current_dir(module_path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !pull_output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to pull: {}",
|
||||||
|
String::from_utf8_lossy(&pull_output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage the submodule update
|
||||||
|
let add_output = std::process::Command::new("git")
|
||||||
|
.args(&["add", &module_info.path])
|
||||||
|
.current_dir(&self.ai_root)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !add_output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to stage submodule: {}",
|
||||||
|
String::from_utf8_lossy(&add_output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auto_commit_changes(&self, updated_modules: &[(String, Option<String>, Option<String>)]) -> Result<()> {
|
||||||
|
println!("{}", "💾 Auto-committing changes...".blue());
|
||||||
|
|
||||||
|
let mut commit_message = format!("Update submodules\n\n📦 Updated modules: {}\n", updated_modules.len());
|
||||||
|
for (module_name, old_commit, new_commit) in updated_modules {
|
||||||
|
commit_message.push_str(&format!(
|
||||||
|
"- {}: {} → {}\n",
|
||||||
|
module_name,
|
||||||
|
old_commit.as_deref().unwrap_or("unknown"),
|
||||||
|
new_commit.as_deref().unwrap_or("unknown")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
commit_message.push_str("\n🤖 Generated with aigpt-rs submodules update");
|
||||||
|
|
||||||
|
let commit_output = std::process::Command::new("git")
|
||||||
|
.args(&["commit", "-m", &commit_message])
|
||||||
|
.current_dir(&self.ai_root)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if commit_output.status.success() {
|
||||||
|
println!("{}", "✅ Changes committed successfully".green());
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Failed to commit: {}",
|
||||||
|
String::from_utf8_lossy(&commit_output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
1099
src/tokens.rs
Normal file
1099
src/tokens.rs
Normal file
File diff suppressed because it is too large
Load Diff
423
src/transmission.rs
Normal file
423
src/transmission.rs
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::persona::Persona;
|
||||||
|
use crate::relationship::{Relationship, RelationshipStatus};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TransmissionLog {
|
||||||
|
pub user_id: String,
|
||||||
|
pub message: String,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub transmission_type: TransmissionType,
|
||||||
|
pub success: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TransmissionType {
|
||||||
|
Autonomous, // AI decided to send
|
||||||
|
Scheduled, // Time-based trigger
|
||||||
|
Breakthrough, // Fortune breakthrough triggered
|
||||||
|
Maintenance, // Daily maintenance message
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TransmissionType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TransmissionType::Autonomous => write!(f, "autonomous"),
|
||||||
|
TransmissionType::Scheduled => write!(f, "scheduled"),
|
||||||
|
TransmissionType::Breakthrough => write!(f, "breakthrough"),
|
||||||
|
TransmissionType::Maintenance => write!(f, "maintenance"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TransmissionController {
|
||||||
|
config: Config,
|
||||||
|
transmission_history: Vec<TransmissionLog>,
|
||||||
|
last_check: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransmissionController {
|
||||||
|
pub fn new(config: Config) -> Result<Self> {
|
||||||
|
let transmission_history = Self::load_transmission_history(&config)?;
|
||||||
|
|
||||||
|
Ok(TransmissionController {
|
||||||
|
config,
|
||||||
|
transmission_history,
|
||||||
|
last_check: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_autonomous_transmissions(&mut self, persona: &mut Persona) -> Result<Vec<TransmissionLog>> {
|
||||||
|
let mut transmissions = Vec::new();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Get all transmission-eligible relationships
|
||||||
|
let eligible_user_ids: Vec<String> = {
|
||||||
|
let relationships = persona.list_all_relationships();
|
||||||
|
relationships.iter()
|
||||||
|
.filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken)
|
||||||
|
.filter(|(_, rel)| rel.score >= rel.threshold)
|
||||||
|
.map(|(id, _)| id.clone())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for user_id in eligible_user_ids {
|
||||||
|
// Get fresh relationship data for each check
|
||||||
|
if let Some(relationship) = persona.get_relationship(&user_id) {
|
||||||
|
// Check if enough time has passed since last transmission
|
||||||
|
if let Some(last_transmission) = relationship.last_transmission {
|
||||||
|
let hours_since_last = (now - last_transmission).num_hours();
|
||||||
|
if hours_since_last < 24 {
|
||||||
|
continue; // Skip if transmitted in last 24 hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if conditions are met for autonomous transmission
|
||||||
|
if self.should_transmit_to_user(&user_id, relationship, persona)? {
|
||||||
|
let transmission = self.generate_autonomous_transmission(persona, &user_id).await?;
|
||||||
|
transmissions.push(transmission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_check = Some(now);
|
||||||
|
self.save_transmission_history()?;
|
||||||
|
|
||||||
|
Ok(transmissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_breakthrough_transmissions(&mut self, persona: &mut Persona) -> Result<Vec<TransmissionLog>> {
|
||||||
|
let mut transmissions = Vec::new();
|
||||||
|
let state = persona.get_current_state()?;
|
||||||
|
|
||||||
|
// Only trigger breakthrough transmissions if fortune is very high
|
||||||
|
if !state.breakthrough_triggered || state.fortune_value < 9 {
|
||||||
|
return Ok(transmissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get close relationships for breakthrough sharing
|
||||||
|
let relationships = persona.list_all_relationships();
|
||||||
|
let close_friends: Vec<_> = relationships.iter()
|
||||||
|
.filter(|(_, rel)| matches!(rel.status, RelationshipStatus::Friend | RelationshipStatus::CloseFriend))
|
||||||
|
.filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (user_id, _relationship) in close_friends {
|
||||||
|
// Check if we haven't sent a breakthrough message today
|
||||||
|
let today = chrono::Utc::now().date_naive();
|
||||||
|
let already_sent_today = self.transmission_history.iter()
|
||||||
|
.any(|log| {
|
||||||
|
log.user_id == *user_id &&
|
||||||
|
matches!(log.transmission_type, TransmissionType::Breakthrough) &&
|
||||||
|
log.timestamp.date_naive() == today
|
||||||
|
});
|
||||||
|
|
||||||
|
if !already_sent_today {
|
||||||
|
let transmission = self.generate_breakthrough_transmission(persona, user_id).await?;
|
||||||
|
transmissions.push(transmission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(transmissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_maintenance_transmissions(&mut self, persona: &mut Persona) -> Result<Vec<TransmissionLog>> {
|
||||||
|
let mut transmissions = Vec::new();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Only send maintenance messages once per day
|
||||||
|
let today = now.date_naive();
|
||||||
|
let already_sent_today = self.transmission_history.iter()
|
||||||
|
.any(|log| {
|
||||||
|
matches!(log.transmission_type, TransmissionType::Maintenance) &&
|
||||||
|
log.timestamp.date_naive() == today
|
||||||
|
});
|
||||||
|
|
||||||
|
if already_sent_today {
|
||||||
|
return Ok(transmissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply daily maintenance to persona
|
||||||
|
persona.daily_maintenance()?;
|
||||||
|
|
||||||
|
// Get relationships that might need a maintenance check-in
|
||||||
|
let relationships = persona.list_all_relationships();
|
||||||
|
let maintenance_candidates: Vec<_> = relationships.iter()
|
||||||
|
.filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken)
|
||||||
|
.filter(|(_, rel)| {
|
||||||
|
// Send maintenance to relationships that haven't been contacted in a while
|
||||||
|
if let Some(last_interaction) = rel.last_interaction {
|
||||||
|
let days_since = (now - last_interaction).num_days();
|
||||||
|
days_since >= 7 // Haven't talked in a week
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.take(3) // Limit to 3 maintenance messages per day
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (user_id, _) in maintenance_candidates {
|
||||||
|
let transmission = self.generate_maintenance_transmission(persona, user_id).await?;
|
||||||
|
transmissions.push(transmission);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(transmissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_transmit_to_user(&self, user_id: &str, relationship: &Relationship, persona: &Persona) -> Result<bool> {
|
||||||
|
// Basic transmission criteria
|
||||||
|
if !relationship.transmission_enabled || relationship.is_broken {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score must be above threshold
|
||||||
|
if relationship.score < relationship.threshold {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check transmission cooldown
|
||||||
|
if let Some(last_transmission) = relationship.last_transmission {
|
||||||
|
let hours_since = (Utc::now() - last_transmission).num_hours();
|
||||||
|
if hours_since < 24 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate transmission probability based on relationship strength
|
||||||
|
let base_probability = match relationship.status {
|
||||||
|
RelationshipStatus::New => 0.1,
|
||||||
|
RelationshipStatus::Acquaintance => 0.2,
|
||||||
|
RelationshipStatus::Friend => 0.4,
|
||||||
|
RelationshipStatus::CloseFriend => 0.6,
|
||||||
|
RelationshipStatus::Broken => 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modify probability based on fortune
|
||||||
|
let state = persona.get_current_state()?;
|
||||||
|
let fortune_modifier = (state.fortune_value as f64 - 5.0) / 10.0; // -0.4 to +0.5
|
||||||
|
let final_probability = (base_probability + fortune_modifier).max(0.0).min(1.0);
|
||||||
|
|
||||||
|
// Simple random check (in real implementation, this would be more sophisticated)
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
user_id.hash(&mut hasher);
|
||||||
|
Utc::now().timestamp().hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
let random_value = (hash % 100) as f64 / 100.0;
|
||||||
|
|
||||||
|
Ok(random_value < final_probability)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_autonomous_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result<TransmissionLog> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Get recent memories for context
|
||||||
|
let memories = persona.get_memories(user_id, 3);
|
||||||
|
let context = if !memories.is_empty() {
|
||||||
|
format!("Based on our recent conversations: {}", memories.join(", "))
|
||||||
|
} else {
|
||||||
|
"Starting a spontaneous conversation".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate message using AI if available
|
||||||
|
let message = match self.generate_ai_message(persona, user_id, &context, TransmissionType::Autonomous).await {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => {
|
||||||
|
// Fallback to simple messages
|
||||||
|
let fallback_messages = [
|
||||||
|
"Hey! How have you been?",
|
||||||
|
"Just thinking about our last conversation...",
|
||||||
|
"Hope you're having a good day!",
|
||||||
|
"Something interesting happened today and it reminded me of you.",
|
||||||
|
];
|
||||||
|
let index = (now.timestamp() as usize) % fallback_messages.len();
|
||||||
|
fallback_messages[index].to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let log = TransmissionLog {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
message,
|
||||||
|
timestamp: now,
|
||||||
|
transmission_type: TransmissionType::Autonomous,
|
||||||
|
success: true, // For now, assume success
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.transmission_history.push(log.clone());
|
||||||
|
Ok(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_breakthrough_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result<TransmissionLog> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let state = persona.get_current_state()?;
|
||||||
|
|
||||||
|
let message = match self.generate_ai_message(persona, user_id, "Breakthrough moment - feeling inspired!", TransmissionType::Breakthrough).await {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => {
|
||||||
|
format!("Amazing day today! ⚡ Fortune is at {}/10 and I'm feeling incredibly inspired. Had to share this energy with you!", state.fortune_value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let log = TransmissionLog {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
message,
|
||||||
|
timestamp: now,
|
||||||
|
transmission_type: TransmissionType::Breakthrough,
|
||||||
|
success: true,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.transmission_history.push(log.clone());
|
||||||
|
Ok(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_maintenance_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result<TransmissionLog> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let message = match self.generate_ai_message(persona, user_id, "Maintenance check-in", TransmissionType::Maintenance).await {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => {
|
||||||
|
"Hey! It's been a while since we last talked. Just checking in to see how you're doing!".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let log = TransmissionLog {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
message,
|
||||||
|
timestamp: now,
|
||||||
|
transmission_type: TransmissionType::Maintenance,
|
||||||
|
success: true,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.transmission_history.push(log.clone());
|
||||||
|
Ok(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_ai_message(&self, _persona: &mut Persona, _user_id: &str, context: &str, transmission_type: TransmissionType) -> Result<String> {
|
||||||
|
// Try to use AI for message generation
|
||||||
|
let _system_prompt = format!(
|
||||||
|
"You are initiating a {} conversation. Context: {}. Keep the message casual, personal, and under 100 characters. Show genuine interest in the person.",
|
||||||
|
transmission_type, context
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is a simplified version - in a real implementation, we'd use the AI provider
|
||||||
|
// For now, return an error to trigger fallback
|
||||||
|
Err(anyhow::anyhow!("AI provider not available for transmission generation"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_eligible_relationships(&self, persona: &Persona) -> Vec<String> {
|
||||||
|
persona.list_all_relationships().iter()
|
||||||
|
.filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken)
|
||||||
|
.filter(|(_, rel)| rel.score >= rel.threshold)
|
||||||
|
.map(|(id, _)| id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transmission_stats(&self) -> TransmissionStats {
|
||||||
|
let total_transmissions = self.transmission_history.len();
|
||||||
|
let successful_transmissions = self.transmission_history.iter()
|
||||||
|
.filter(|log| log.success)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
let today_transmissions = self.transmission_history.iter()
|
||||||
|
.filter(|log| log.timestamp.date_naive() == today)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let by_type = {
|
||||||
|
let mut counts = HashMap::new();
|
||||||
|
for log in &self.transmission_history {
|
||||||
|
*counts.entry(log.transmission_type.to_string()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
counts
|
||||||
|
};
|
||||||
|
|
||||||
|
TransmissionStats {
|
||||||
|
total_transmissions,
|
||||||
|
successful_transmissions,
|
||||||
|
today_transmissions,
|
||||||
|
success_rate: if total_transmissions > 0 {
|
||||||
|
successful_transmissions as f64 / total_transmissions as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
by_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_recent_transmissions(&self, limit: usize) -> Vec<&TransmissionLog> {
|
||||||
|
let mut logs: Vec<_> = self.transmission_history.iter().collect();
|
||||||
|
logs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||||
|
logs.into_iter().take(limit).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_transmission_history(config: &Config) -> Result<Vec<TransmissionLog>> {
|
||||||
|
let file_path = config.transmission_file();
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.context("Failed to read transmission history file")?;
|
||||||
|
|
||||||
|
let history: Vec<TransmissionLog> = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse transmission history file")?;
|
||||||
|
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_transmission_history(&self) -> Result<()> {
|
||||||
|
let content = serde_json::to_string_pretty(&self.transmission_history)
|
||||||
|
.context("Failed to serialize transmission history")?;
|
||||||
|
|
||||||
|
std::fs::write(&self.config.transmission_file(), content)
|
||||||
|
.context("Failed to write transmission history file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_and_send(&mut self) -> Result<Vec<(String, String)>> {
|
||||||
|
let config = self.config.clone();
|
||||||
|
let mut persona = Persona::new(&config)?;
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
// Check autonomous transmissions
|
||||||
|
let autonomous = self.check_autonomous_transmissions(&mut persona).await?;
|
||||||
|
for log in autonomous {
|
||||||
|
if log.success {
|
||||||
|
results.push((log.user_id, log.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check breakthrough transmissions
|
||||||
|
let breakthrough = self.check_breakthrough_transmissions(&mut persona).await?;
|
||||||
|
for log in breakthrough {
|
||||||
|
if log.success {
|
||||||
|
results.push((log.user_id, log.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TransmissionStats {
|
||||||
|
pub total_transmissions: usize,
|
||||||
|
pub successful_transmissions: usize,
|
||||||
|
pub today_transmissions: usize,
|
||||||
|
pub success_rate: f64,
|
||||||
|
pub by_type: HashMap<String, usize>,
|
||||||
|
}
|
13
src/utils.rs
13
src/utils.rs
@ -1,13 +0,0 @@
|
|||||||
// src/utils.rs
|
|
||||||
use std::fs;
|
|
||||||
use crate::model::AiSystem;
|
|
||||||
|
|
||||||
pub fn load_config(path: &str) -> AiSystem {
|
|
||||||
let data = fs::read_to_string(path).expect("JSON読み込み失敗");
|
|
||||||
serde_json::from_str(&data).expect("JSONパース失敗")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_config(path: &str, ai: &AiSystem) {
|
|
||||||
let json = serde_json::to_string_pretty(&ai).expect("JSONシリアライズ失敗");
|
|
||||||
fs::write(path, json).expect("JSON保存失敗");
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::io::{self, Write};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
if args.len() < 2 {
|
|
||||||
eprintln!("Usage: langchain_cli <prompt>");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let prompt = &args[1];
|
|
||||||
|
|
||||||
// Simulate a pipeline stage: e.g., tokenization, reasoning, response generation
|
|
||||||
let stages = vec!["Tokenize", "Reason", "Generate"];
|
|
||||||
|
|
||||||
for stage in &stages {
|
|
||||||
println!("[Stage: {}] Processing...", stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example call to Python-based LangChain (assuming you have a script or API to call)
|
|
||||||
// For placeholder purposes, we echo the prompt back.
|
|
||||||
let output = Command::new("python3")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(format!("print(\"LangChain Agent Response for: {}\")", prompt))
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.expect("failed to execute process")
|
|
||||||
.wait_with_output()
|
|
||||||
.expect("failed to wait on child");
|
|
||||||
|
|
||||||
io::stdout().write_all(&output.stdout).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO (for future LangChain-style pipeline):
|
|
||||||
1. Implement trait-based agent components: Tokenizer, Retriever, Reasoner, Generator.
|
|
||||||
2. Allow config via YAML or TOML to define chain flow.
|
|
||||||
3. Async pipeline support with Tokio.
|
|
||||||
4. Optional integration with LLM APIs (OpenAI, Ollama, etc).
|
|
||||||
5. Rust-native vector search (e.g. using `tantivy`, `qdrant-client`).
|
|
||||||
*/
|
|
133
tmp/data.rs
133
tmp/data.rs
@ -1,133 +0,0 @@
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RelationalAutonomousAI {
|
|
||||||
pub system_name: String,
|
|
||||||
pub description: String,
|
|
||||||
pub core_components: CoreComponents,
|
|
||||||
pub extensions: Extensions,
|
|
||||||
pub note: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct CoreComponents {
|
|
||||||
pub personality: Personality,
|
|
||||||
pub relationship: Relationship,
|
|
||||||
pub environment: Environment,
|
|
||||||
pub memory: Memory,
|
|
||||||
pub message_trigger: MessageTrigger,
|
|
||||||
pub message_generation: MessageGeneration,
|
|
||||||
pub state_transition: StateTransition,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Personality {
|
|
||||||
pub r#type: String,
|
|
||||||
pub variants: Vec<String>,
|
|
||||||
pub parameters: PersonalityParameters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct PersonalityParameters {
|
|
||||||
pub message_trigger_style: String,
|
|
||||||
pub decay_rate_modifier: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Relationship {
|
|
||||||
pub parameters: Vec<String>,
|
|
||||||
pub properties: RelationshipProperties,
|
|
||||||
pub decay_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RelationshipProperties {
|
|
||||||
pub persistent: bool,
|
|
||||||
pub hidden: bool,
|
|
||||||
pub irreversible: bool,
|
|
||||||
pub decay_over_time: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Environment {
|
|
||||||
pub daily_luck: DailyLuck,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct DailyLuck {
|
|
||||||
pub r#type: String,
|
|
||||||
pub range: Vec<f32>,
|
|
||||||
pub update: String,
|
|
||||||
pub streak_mechanism: StreakMechanism,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct StreakMechanism {
|
|
||||||
pub trigger: String,
|
|
||||||
pub effect: String,
|
|
||||||
pub chance: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Memory {
|
|
||||||
pub long_term_memory: String,
|
|
||||||
pub short_term_context: String,
|
|
||||||
pub usage_in_generation: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct MessageTrigger {
|
|
||||||
pub condition: TriggerCondition,
|
|
||||||
pub timing: TriggerTiming,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct TriggerCondition {
|
|
||||||
pub relationship_threshold: String,
|
|
||||||
pub time_decay: bool,
|
|
||||||
pub environment_luck: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct TriggerTiming {
|
|
||||||
pub based_on: Vec<String>,
|
|
||||||
pub modifiers: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct MessageGeneration {
|
|
||||||
pub style_variants: Vec<String>,
|
|
||||||
pub influenced_by: Vec<String>,
|
|
||||||
pub llm_integration: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct StateTransition {
|
|
||||||
pub states: Vec<String>,
|
|
||||||
pub transitions: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Extensions {
|
|
||||||
pub persistence: Persistence,
|
|
||||||
pub api: Api,
|
|
||||||
pub scheduler: Scheduler,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Persistence {
|
|
||||||
pub database: String,
|
|
||||||
pub storage_items: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Api {
|
|
||||||
pub llm: String,
|
|
||||||
pub mode: String,
|
|
||||||
pub external_event_trigger: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Scheduler {
|
|
||||||
pub async_event_loop: bool,
|
|
||||||
pub interval_check: i32,
|
|
||||||
pub time_decay_check: bool,
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1,46 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, Write};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
mod model;
|
|
||||||
use model::RelationalAutonomousAI;
|
|
||||||
|
|
||||||
fn load_config(path: &str) -> std::io::Result<RelationalAutonomousAI> {
|
|
||||||
let file = File::open(path)?;
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
let config: RelationalAutonomousAI = serde_json::from_reader(reader)?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_config(config: &RelationalAutonomousAI, path: &str) -> std::io::Result<()> {
|
|
||||||
let mut file = File::create(path)?;
|
|
||||||
let json = serde_json::to_string_pretty(config)?;
|
|
||||||
file.write_all(json.as_bytes())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_send_message(config: &RelationalAutonomousAI) -> bool {
|
|
||||||
// 簡易な送信条件: relationshipが高く、daily_luckが0.8以上
|
|
||||||
config.core_components.relationship.parameters.contains(&"trust".to_string())
|
|
||||||
&& config.core_components.environment.daily_luck.range[1] >= 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
|
||||||
let path = "config.json";
|
|
||||||
|
|
||||||
let mut config = load_config(path)?;
|
|
||||||
|
|
||||||
if should_send_message(&config) {
|
|
||||||
println!("💌 メッセージを送信できます: {:?}", config.core_components.personality.r#type);
|
|
||||||
|
|
||||||
// ステート変化の例: メッセージ送信後に記録用トランジションを追加
|
|
||||||
config.core_components.state_transition.transitions.push("message_sent".to_string());
|
|
||||||
|
|
||||||
save_config(&config, path)?;
|
|
||||||
} else {
|
|
||||||
println!("😶 まだ送信条件に達していません。");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
Reference in New Issue
Block a user