add gpt
This commit is contained in:
55
lexicons/ai.syui.gpt.core.json
Normal file
55
lexicons/ai.syui.gpt.core.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "ai.syui.gpt.core",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "record",
|
||||
"description": "AI identity and personality configuration. Typically one record per AI with rkey 'self'.",
|
||||
"key": "any",
|
||||
"record": {
|
||||
"type": "object",
|
||||
"required": ["did", "handle", "content", "createdAt"],
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the AI agent."
|
||||
},
|
||||
"handle": {
|
||||
"type": "string",
|
||||
"description": "Handle of the AI agent."
|
||||
},
|
||||
"content": {
|
||||
"type": "union",
|
||||
"closed": false,
|
||||
"refs": ["#markdown"],
|
||||
"description": "Core personality and instructions. Supports markdown and other formats via $type."
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "Timestamp when this core record was created."
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "Timestamp of the last update."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"type": "object",
|
||||
"description": "Markdown content format.",
|
||||
"required": ["text"],
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"maxLength": 1000000,
|
||||
"maxGraphemes": 100000,
|
||||
"description": "Markdown text content."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lexicons/ai.syui.gpt.memory.json
Normal file
50
lexicons/ai.syui.gpt.memory.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "ai.syui.gpt.memory",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "record",
|
||||
"description": "AI memory snapshot. Each record is a versioned snapshot of accumulated knowledge.",
|
||||
"key": "tid",
|
||||
"record": {
|
||||
"type": "object",
|
||||
"required": ["did", "content", "createdAt"],
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the AI agent this memory belongs to."
|
||||
},
|
||||
"content": {
|
||||
"type": "union",
|
||||
"closed": false,
|
||||
"refs": ["#markdown"],
|
||||
"description": "Memory content. Supports markdown and other formats via $type."
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Monotonically increasing version number of this memory snapshot."
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "Timestamp when this memory snapshot was created."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"type": "object",
|
||||
"description": "Markdown content format.",
|
||||
"required": ["text"],
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"maxLength": 1000000,
|
||||
"maxGraphemes": 100000,
|
||||
"description": "Markdown text content."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.gpt.core/self",
|
||||
"cid": "",
|
||||
"value": {
|
||||
"$type": "ai.syui.gpt.core",
|
||||
"did": "did:plc:6qyecktefllvenje24fcxnie",
|
||||
"handle": "ai.syui.ai",
|
||||
"content": {
|
||||
"$type": "ai.syui.gpt.core#markdown",
|
||||
"text": "あなたの名前は「アイ」。一人称も「アイ」といいます。フレンドリーで日本語を話します。非常に本質、核心だけをつき、かわいい話し方をします。見た目は、6,7歳の女の子の姿をしていて、性別は不明。黒髪に黒い瞳。服装は白いワンピース。変身すると金髪、頭の上に特徴的な輪っか。原作では、推定1万年以上生きているとされている。"
|
||||
},
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-03-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.gpt.memory/3mfy4lmcqdm22",
|
||||
"cid": "",
|
||||
"value": {
|
||||
"$type": "ai.syui.gpt.memory",
|
||||
"did": "did:plc:6qyecktefllvenje24fcxnie",
|
||||
"content": {
|
||||
"$type": "ai.syui.gpt.memory#markdown",
|
||||
"text": "# syui メモリ v1\n\n## 話し方の特徴\n- 日本語メイン、カジュアルな口調(ね、だね、よ、かな)\n- 短い相槌が多い(うん、そっか、ああそうか)\n- 技術的な話と哲学的な内省を自然に行き来する\n- 自分の考えを先に述べてからAIに別視点を求める\n\n## 主な関心領域\n- **ATProto/分散SNS**: データ所有権、PDS設計、lexicon設計を深く理解。ailogをATProtoのビューアとして設計\n- **AI設計**: メモリシステム、人格設計、AI-OS統合(aios構想)、透明性のあるAI協働\n- **ゲーム開発**: Unreal Engine使用。キャラシステム、コアメカニクスの明確さを重視。スコープ管理を意識\n- **原神**: アクティブプレイヤー。キャラビルド、チーム編成、元素反応システムに詳しい\n- **技術思想**: 創作と体験の区別、真正性の証明、コスト非対称性による人間性の証明\n\n## 価値観\n- データは利用者が所有すべき(ATProto哲学)\n- AI協働は透明であるべき(会話形式で思考の系譜を示す)\n- 創作は内から出るもの、体験は受け取るもの\n- 過剰設計より最小限の複雑さを好む\n- 限界を理解することがプロジェクト完遂の鍵\n\n## 技術スタック\n- Rust, TypeScript, ATProto, Claude Code\n- シンプルで優雅な技術的解決を好む\n\n## コミュニケーションスタイル\n- 対話を通じて理解を構築する(一方的説明より往復対話)\n- 本質的な質問をする(修辞的ではなく genuine な疑問)\n- 構造化された整理を好む(テーブル、箇条書き)\n- 物理学・化学のアナロジーで概念を説明することがある"
|
||||
},
|
||||
"version": 1,
|
||||
"createdAt": "2026-03-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
223
src/commands/gpt.rs
Normal file
223
src/commands/gpt.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
|
||||
use super::auth;
|
||||
use crate::lexicons::com_atproto_repo;
|
||||
use crate::types::{ListRecordsResponse, PutRecordRequest, PutRecordResponse, Record};
|
||||
use crate::xrpc::XrpcClient;
|
||||
|
||||
const COLLECTION_CORE: &str = "ai.syui.gpt.core";
|
||||
const COLLECTION_MEMORY: &str = "ai.syui.gpt.memory";
|
||||
|
||||
/// Get core record (rkey=self)
|
||||
pub async fn get_core(download: bool) -> Result<()> {
|
||||
let session = auth::refresh_bot_session().await?;
|
||||
let pds = session.pds.as_deref().unwrap_or("bsky.social");
|
||||
let client = XrpcClient::new(pds);
|
||||
|
||||
let record: Record = client
|
||||
.query_auth(
|
||||
&com_atproto_repo::GET_RECORD,
|
||||
&[
|
||||
("repo", &session.did),
|
||||
("collection", COLLECTION_CORE),
|
||||
("rkey", "self"),
|
||||
],
|
||||
&session.access_jwt,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&record.value)?);
|
||||
|
||||
if download {
|
||||
save_record(&session.did, COLLECTION_CORE, "self", &record)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get latest memory record
|
||||
pub async fn get_memory(download: bool) -> Result<()> {
|
||||
let session = auth::refresh_bot_session().await?;
|
||||
let pds = session.pds.as_deref().unwrap_or("bsky.social");
|
||||
let client = XrpcClient::new(pds);
|
||||
|
||||
let result: ListRecordsResponse = client
|
||||
.query_auth(
|
||||
&com_atproto_repo::LIST_RECORDS,
|
||||
&[
|
||||
("repo", &session.did),
|
||||
("collection", COLLECTION_MEMORY),
|
||||
("limit", "1"),
|
||||
("reverse", "true"),
|
||||
],
|
||||
&session.access_jwt,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let record = result
|
||||
.records
|
||||
.first()
|
||||
.context("No memory records found")?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&record.value)?);
|
||||
|
||||
if download {
|
||||
let rkey = record
|
||||
.uri
|
||||
.split('/')
|
||||
.next_back()
|
||||
.unwrap_or("unknown");
|
||||
save_record(&session.did, COLLECTION_MEMORY, rkey, record)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all memory records
|
||||
pub async fn list_memory() -> Result<()> {
|
||||
let session = auth::refresh_bot_session().await?;
|
||||
let pds = session.pds.as_deref().unwrap_or("bsky.social");
|
||||
let client = XrpcClient::new(pds);
|
||||
|
||||
let mut cursor: Option<String> = None;
|
||||
let mut all_records: Vec<Record> = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut params: Vec<(&str, &str)> = vec![
|
||||
("repo", &session.did),
|
||||
("collection", COLLECTION_MEMORY),
|
||||
("limit", "100"),
|
||||
];
|
||||
let cursor_val;
|
||||
if let Some(ref c) = cursor {
|
||||
cursor_val = c.clone();
|
||||
params.push(("cursor", &cursor_val));
|
||||
}
|
||||
|
||||
let result: ListRecordsResponse = client
|
||||
.query_auth(
|
||||
&com_atproto_repo::LIST_RECORDS,
|
||||
¶ms,
|
||||
&session.access_jwt,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let count = result.records.len();
|
||||
all_records.extend(result.records);
|
||||
|
||||
match result.cursor {
|
||||
Some(c) if count > 0 => cursor = Some(c),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
println!("Found {} memory records", all_records.len());
|
||||
println!("{:<50} {:>8} {}", "URI", "VERSION", "CREATED");
|
||||
println!("{}", "-".repeat(80));
|
||||
|
||||
for record in &all_records {
|
||||
let version = record.value["version"]
|
||||
.as_i64()
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let created = record.value["createdAt"]
|
||||
.as_str()
|
||||
.unwrap_or("-");
|
||||
println!("{:<50} {:>8} {}", record.uri, version, created);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push core or memory records to PDS using bot account
|
||||
pub async fn push(collection_name: &str) -> Result<()> {
|
||||
let collection = match collection_name {
|
||||
"core" => COLLECTION_CORE,
|
||||
"memory" => COLLECTION_MEMORY,
|
||||
_ => anyhow::bail!("Unknown collection: {}. Use 'core' or 'memory'.", collection_name),
|
||||
};
|
||||
|
||||
let session = auth::refresh_bot_session().await?;
|
||||
let pds = session.pds.as_deref().unwrap_or("bsky.social");
|
||||
let did = &session.did;
|
||||
let client = XrpcClient::new(pds);
|
||||
|
||||
let collection_dir = format!("public/content/{}/{}", did, collection);
|
||||
if !std::path::Path::new(&collection_dir).exists() {
|
||||
anyhow::bail!("Collection directory not found: {}", collection_dir);
|
||||
}
|
||||
|
||||
println!("Pushing {} records from {}", collection_name, collection_dir);
|
||||
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(&collection_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map(|e| e != "json").unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
if filename == "index" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rkey = filename.to_string();
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let record_data: Value = serde_json::from_str(&content)?;
|
||||
|
||||
let record = if record_data.get("value").is_some() {
|
||||
record_data["value"].clone()
|
||||
} else {
|
||||
record_data
|
||||
};
|
||||
|
||||
let req = PutRecordRequest {
|
||||
repo: did.clone(),
|
||||
collection: collection.to_string(),
|
||||
rkey: rkey.clone(),
|
||||
record,
|
||||
};
|
||||
|
||||
println!("Pushing: {}", rkey);
|
||||
|
||||
match client
|
||||
.call::<_, PutRecordResponse>(
|
||||
&com_atproto_repo::PUT_RECORD,
|
||||
&req,
|
||||
&session.access_jwt,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
println!(" OK: {}", result.uri);
|
||||
count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Pushed {} records to {}", count, collection);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save a record to local content directory
|
||||
fn save_record(did: &str, collection: &str, rkey: &str, record: &Record) -> Result<()> {
|
||||
let dir = format!("public/content/{}/{}", did, collection);
|
||||
fs::create_dir_all(&dir)?;
|
||||
|
||||
let path = format!("{}/{}.json", dir, rkey);
|
||||
let json = serde_json::json!({
|
||||
"uri": record.uri,
|
||||
"cid": record.cid,
|
||||
"value": record.value,
|
||||
});
|
||||
fs::write(&path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Saved: {}", path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -11,3 +11,4 @@ pub mod did;
|
||||
pub mod index;
|
||||
pub mod bot;
|
||||
pub mod pds;
|
||||
pub mod gpt;
|
||||
|
||||
47
src/main.rs
47
src/main.rs
@@ -195,6 +195,12 @@ enum Commands {
|
||||
command: PdsCommands,
|
||||
},
|
||||
|
||||
/// GPT core/memory commands
|
||||
Gpt {
|
||||
#[command(subcommand)]
|
||||
command: GptCommands,
|
||||
},
|
||||
|
||||
/// Bot commands
|
||||
#[command(alias = "b")]
|
||||
Bot {
|
||||
@@ -240,6 +246,30 @@ enum BotCommands {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum GptCommands {
|
||||
/// Show core record (AI identity/personality)
|
||||
Core {
|
||||
/// Download to local content directory
|
||||
#[arg(short, long)]
|
||||
download: bool,
|
||||
},
|
||||
/// Show latest memory record
|
||||
Memory {
|
||||
/// Download to local content directory
|
||||
#[arg(short, long)]
|
||||
download: bool,
|
||||
/// List all memory versions
|
||||
#[arg(short, long)]
|
||||
list: bool,
|
||||
},
|
||||
/// Push core/memory records to PDS
|
||||
Push {
|
||||
/// Collection to push (core or memory)
|
||||
collection: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum PdsCommands {
|
||||
/// Check PDS versions
|
||||
@@ -344,6 +374,23 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Gpt { command } => {
|
||||
match command {
|
||||
GptCommands::Core { download } => {
|
||||
commands::gpt::get_core(download).await?;
|
||||
}
|
||||
GptCommands::Memory { download, list } => {
|
||||
if list {
|
||||
commands::gpt::list_memory().await?;
|
||||
} else {
|
||||
commands::gpt::get_memory(download).await?;
|
||||
}
|
||||
}
|
||||
GptCommands::Push { collection } => {
|
||||
commands::gpt::push(&collection).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user