nao-core 0.0.9__py3-none-any.whl → 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nao_core/__init__.py +1 -2
- nao_core/commands/__init__.py +3 -2
- nao_core/commands/chat.py +106 -114
- nao_core/commands/debug.py +144 -0
- nao_core/commands/init.py +151 -48
- nao_core/commands/sync.py +150 -0
- nao_core/config.py +122 -50
- nao_core/main.py +4 -2
- nao_core-0.0.11.dist-info/METADATA +130 -0
- nao_core-0.0.11.dist-info/RECORD +20 -0
- nao_core-0.0.9.dist-info/METADATA +0 -93
- nao_core-0.0.9.dist-info/RECORD +0 -18
- {nao_core-0.0.9.dist-info → nao_core-0.0.11.dist-info}/WHEEL +0 -0
- {nao_core-0.0.9.dist-info → nao_core-0.0.11.dist-info}/entry_points.txt +0 -0
- {nao_core-0.0.9.dist-info → nao_core-0.0.11.dist-info}/licenses/LICENSE +0 -0
nao_core/__init__.py
CHANGED
nao_core/commands/__init__.py
CHANGED
nao_core/commands/chat.py
CHANGED
|
@@ -16,125 +16,117 @@ SERVER_PORT = 5005
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def get_server_binary_path() -> Path:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
"""Get the path to the bundled nao-chat-server binary."""
|
|
20
|
+
# The binary is in the bin folder relative to this file
|
|
21
|
+
cli_dir = Path(__file__).parent.parent
|
|
22
|
+
bin_dir = cli_dir / "bin"
|
|
23
|
+
binary_path = bin_dir / "nao-chat-server"
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.print(
|
|
30
|
-
"[dim]Make sure you've built the server with ./scripts/build-server.sh[/dim]"
|
|
31
|
-
)
|
|
32
|
-
sys.exit(1)
|
|
25
|
+
if not binary_path.exists():
|
|
26
|
+
console.print(f"[bold red]✗[/bold red] Server binary not found at {binary_path}")
|
|
27
|
+
console.print("[dim]Make sure you've built the server with ./scripts/build-server.sh[/dim]")
|
|
28
|
+
sys.exit(1)
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
return binary_path
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
def wait_for_server(port: int, timeout: int = 30) -> bool:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
"""Wait for the server to be ready."""
|
|
35
|
+
import socket
|
|
36
|
+
|
|
37
|
+
for _ in range(timeout * 10): # Check every 100ms
|
|
38
|
+
try:
|
|
39
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
40
|
+
sock.settimeout(0.1)
|
|
41
|
+
result = sock.connect_ex(("localhost", port))
|
|
42
|
+
if result == 0:
|
|
43
|
+
return True
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
sleep(0.1)
|
|
47
|
+
return False
|
|
52
48
|
|
|
53
49
|
|
|
54
50
|
def chat():
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.print(f"[bold red]✗[/bold red] Failed to start server: {e}")
|
|
138
|
-
if process:
|
|
139
|
-
process.kill()
|
|
140
|
-
sys.exit(1)
|
|
51
|
+
"""Start the nao chat UI.
|
|
52
|
+
|
|
53
|
+
Launches the nao chat server and opens the web interface in your browser.
|
|
54
|
+
"""
|
|
55
|
+
console.print("\n[bold cyan]💬 Starting nao chat...[/bold cyan]\n")
|
|
56
|
+
|
|
57
|
+
binary_path = get_server_binary_path()
|
|
58
|
+
bin_dir = binary_path.parent
|
|
59
|
+
|
|
60
|
+
console.print(f"[dim]Server binary: {binary_path}[/dim]")
|
|
61
|
+
console.print(f"[dim]Working directory: {bin_dir}[/dim]")
|
|
62
|
+
|
|
63
|
+
# Try to load nao config from current directory
|
|
64
|
+
config = NaoConfig.try_load()
|
|
65
|
+
if config:
|
|
66
|
+
console.print(f"[bold green]✓[/bold green] Loaded config from {Path.cwd() / 'nao_config.yaml'}")
|
|
67
|
+
else:
|
|
68
|
+
console.print("[dim]No nao_config.yaml found in current directory[/dim]")
|
|
69
|
+
|
|
70
|
+
# Start the server process
|
|
71
|
+
process = None
|
|
72
|
+
try:
|
|
73
|
+
# Set up environment - inherit from parent but ensure we're in the bin dir
|
|
74
|
+
# so the server can find the public folder
|
|
75
|
+
env = os.environ.copy()
|
|
76
|
+
|
|
77
|
+
# Set LLM API key from config if available
|
|
78
|
+
if config and config.llm:
|
|
79
|
+
env_var_name = f"{config.llm.provider.upper()}_API_KEY"
|
|
80
|
+
env[env_var_name] = config.llm.api_key
|
|
81
|
+
console.print(f"[bold green]✓[/bold green] Set {env_var_name} from config")
|
|
82
|
+
|
|
83
|
+
process = subprocess.Popen(
|
|
84
|
+
[str(binary_path)],
|
|
85
|
+
cwd=str(bin_dir),
|
|
86
|
+
env=env,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.STDOUT,
|
|
89
|
+
text=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
console.print("[bold green]✓[/bold green] Server starting...")
|
|
93
|
+
|
|
94
|
+
# Wait for the server to be ready
|
|
95
|
+
if wait_for_server(SERVER_PORT):
|
|
96
|
+
url = f"http://localhost:{SERVER_PORT}"
|
|
97
|
+
console.print(f"[bold green]✓[/bold green] Server ready at {url}")
|
|
98
|
+
console.print("\n[bold]Opening browser...[/bold]")
|
|
99
|
+
webbrowser.open(url)
|
|
100
|
+
console.print("\n[dim]Press Ctrl+C to stop the server[/dim]\n")
|
|
101
|
+
else:
|
|
102
|
+
console.print("[bold yellow]⚠[/bold yellow] Server is taking longer than expected to start...")
|
|
103
|
+
console.print(f"[dim]Check http://localhost:{SERVER_PORT} manually[/dim]")
|
|
104
|
+
|
|
105
|
+
# Stream server output to console
|
|
106
|
+
if process.stdout:
|
|
107
|
+
for line in process.stdout:
|
|
108
|
+
# Filter out some of the verbose logging if needed
|
|
109
|
+
console.print(f"[dim]{line.rstrip()}[/dim]")
|
|
110
|
+
|
|
111
|
+
# Wait for process to complete
|
|
112
|
+
process.wait()
|
|
113
|
+
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
console.print("\n[bold yellow]Shutting down...[/bold yellow]")
|
|
116
|
+
if process:
|
|
117
|
+
# Send SIGTERM for graceful shutdown
|
|
118
|
+
process.terminate()
|
|
119
|
+
try:
|
|
120
|
+
process.wait(timeout=5)
|
|
121
|
+
except subprocess.TimeoutExpired:
|
|
122
|
+
# Force kill if it doesn't respond
|
|
123
|
+
process.kill()
|
|
124
|
+
process.wait()
|
|
125
|
+
console.print("[bold green]✓[/bold green] Server stopped")
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
console.print(f"[bold red]✗[/bold red] Failed to start server: {e}")
|
|
130
|
+
if process:
|
|
131
|
+
process.kill()
|
|
132
|
+
sys.exit(1)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from nao_core.config import NaoConfig
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_database_connection(db_config) -> tuple[bool, str]:
|
|
12
|
+
"""Test connectivity to a database.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Tuple of (success, message)
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
conn = db_config.connect()
|
|
19
|
+
# Run a simple query to verify the connection works
|
|
20
|
+
if db_config.dataset_id:
|
|
21
|
+
# If dataset is specified, list tables in that dataset
|
|
22
|
+
tables = conn.list_tables()
|
|
23
|
+
table_count = len(tables)
|
|
24
|
+
return True, f"Connected successfully ({table_count} tables found)"
|
|
25
|
+
else:
|
|
26
|
+
# If no dataset, list datasets in the project instead
|
|
27
|
+
datasets = conn.list_databases()
|
|
28
|
+
dataset_count = len(datasets)
|
|
29
|
+
return True, f"Connected successfully ({dataset_count} datasets found)"
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return False, str(e)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_llm_connection(llm_config) -> tuple[bool, str]:
|
|
35
|
+
"""Test connectivity to an LLM provider.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (success, message)
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
if llm_config.provider.value == "openai":
|
|
42
|
+
import openai
|
|
43
|
+
|
|
44
|
+
client = openai.OpenAI(api_key=llm_config.api_key)
|
|
45
|
+
# Make a minimal API call to verify the key works
|
|
46
|
+
models = client.models.list()
|
|
47
|
+
# Just check we can iterate (don't need to consume all)
|
|
48
|
+
model_count = sum(1 for _ in models)
|
|
49
|
+
return True, f"Connected successfully ({model_count} models available)"
|
|
50
|
+
else:
|
|
51
|
+
return False, f"Unknown provider: {llm_config.provider}"
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return False, str(e)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def debug():
|
|
57
|
+
"""Test connectivity to configured databases and LLMs.
|
|
58
|
+
|
|
59
|
+
Loads the nao configuration from the current directory and tests
|
|
60
|
+
connections to all configured databases and LLM providers.
|
|
61
|
+
"""
|
|
62
|
+
console.print("\n[bold cyan]🔍 nao debug - Testing connections...[/bold cyan]\n")
|
|
63
|
+
|
|
64
|
+
# Load config
|
|
65
|
+
config = NaoConfig.try_load()
|
|
66
|
+
if not config:
|
|
67
|
+
console.print("[bold red]✗[/bold red] No nao_config.yaml found in current directory")
|
|
68
|
+
console.print("[dim]Run 'nao init' to create a configuration file[/dim]")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
console.print(f"[bold green]✓[/bold green] Loaded config: [cyan]{config.project_name}[/cyan]\n")
|
|
72
|
+
|
|
73
|
+
# Test databases
|
|
74
|
+
if config.databases:
|
|
75
|
+
console.print("[bold]Databases:[/bold]")
|
|
76
|
+
db_table = Table(show_header=True, header_style="bold")
|
|
77
|
+
db_table.add_column("Name")
|
|
78
|
+
db_table.add_column("Type")
|
|
79
|
+
db_table.add_column("Status")
|
|
80
|
+
db_table.add_column("Details")
|
|
81
|
+
|
|
82
|
+
for db in config.databases:
|
|
83
|
+
console.print(f" Testing [cyan]{db.name}[/cyan]...", end=" ")
|
|
84
|
+
success, message = test_database_connection(db)
|
|
85
|
+
|
|
86
|
+
if success:
|
|
87
|
+
console.print("[bold green]✓[/bold green]")
|
|
88
|
+
db_table.add_row(
|
|
89
|
+
db.name,
|
|
90
|
+
db.type,
|
|
91
|
+
"[green]Connected[/green]",
|
|
92
|
+
message,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
console.print("[bold red]✗[/bold red]")
|
|
96
|
+
# Truncate long error messages
|
|
97
|
+
short_msg = message[:80] + "..." if len(message) > 80 else message
|
|
98
|
+
db_table.add_row(
|
|
99
|
+
db.name,
|
|
100
|
+
db.type,
|
|
101
|
+
"[red]Failed[/red]",
|
|
102
|
+
short_msg,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
console.print()
|
|
106
|
+
console.print(db_table)
|
|
107
|
+
else:
|
|
108
|
+
console.print("[dim]No databases configured[/dim]")
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
|
|
112
|
+
# Test LLM
|
|
113
|
+
if config.llm:
|
|
114
|
+
console.print("[bold]LLM Provider:[/bold]")
|
|
115
|
+
llm_table = Table(show_header=True, header_style="bold")
|
|
116
|
+
llm_table.add_column("Provider")
|
|
117
|
+
llm_table.add_column("Status")
|
|
118
|
+
llm_table.add_column("Details")
|
|
119
|
+
|
|
120
|
+
console.print(f" Testing [cyan]{config.llm.provider.value}[/cyan]...", end=" ")
|
|
121
|
+
success, message = test_llm_connection(config.llm)
|
|
122
|
+
|
|
123
|
+
if success:
|
|
124
|
+
console.print("[bold green]✓[/bold green]")
|
|
125
|
+
llm_table.add_row(
|
|
126
|
+
config.llm.provider.value,
|
|
127
|
+
"[green]Connected[/green]",
|
|
128
|
+
message,
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
console.print("[bold red]✗[/bold red]")
|
|
132
|
+
short_msg = message[:80] + "..." if len(message) > 80 else message
|
|
133
|
+
llm_table.add_row(
|
|
134
|
+
config.llm.provider.value,
|
|
135
|
+
"[red]Failed[/red]",
|
|
136
|
+
short_msg,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
console.print()
|
|
140
|
+
console.print(llm_table)
|
|
141
|
+
else:
|
|
142
|
+
console.print("[dim]No LLM configured[/dim]")
|
|
143
|
+
|
|
144
|
+
console.print()
|
nao_core/commands/init.py
CHANGED
|
@@ -1,72 +1,175 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
2
3
|
|
|
4
|
+
from cyclopts import Parameter
|
|
3
5
|
from rich.console import Console
|
|
4
6
|
from rich.prompt import Confirm, Prompt
|
|
5
7
|
|
|
6
|
-
from nao_core.config import LLMConfig, LLMProvider, NaoConfig
|
|
8
|
+
from nao_core.config import BigQueryConfig, DatabaseConfig, DatabaseType, LLMConfig, LLMProvider, NaoConfig
|
|
7
9
|
|
|
8
10
|
console = Console()
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
class InitError(Exception):
|
|
14
|
+
"""Base exception for init command errors."""
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
console.print("\n[bold cyan]🚀 nao project initialization[/bold cyan]\n")
|
|
16
|
+
pass
|
|
17
17
|
|
|
18
|
-
project_name = Prompt.ask("[bold]Enter your project name[/bold]")
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
19
|
+
class EmptyProjectNameError(InitError):
|
|
20
|
+
"""Raised when project name is empty."""
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__("Project name cannot be empty.")
|
|
25
24
|
|
|
26
|
-
if project_path.exists():
|
|
27
|
-
console.print(
|
|
28
|
-
f"[bold red]✗[/bold red] Folder [yellow]'{project_name}'[/yellow] already exists."
|
|
29
|
-
)
|
|
30
|
-
return
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setup_llm = Confirm.ask("\n[bold]Set up LLM configuration?[/bold]", default=True)
|
|
26
|
+
class ProjectExistsError(InitError):
|
|
27
|
+
"""Raised when project folder already exists."""
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
def __init__(self, project_name: str):
|
|
30
|
+
self.project_name = project_name
|
|
31
|
+
super().__init__(f"Folder '{project_name}' already exists.")
|
|
38
32
|
|
|
39
|
-
provider_choices = [p.value for p in LLMProvider]
|
|
40
|
-
llm_provider = Prompt.ask(
|
|
41
|
-
"[bold]Select LLM provider[/bold]",
|
|
42
|
-
choices=provider_choices,
|
|
43
|
-
default=provider_choices[0],
|
|
44
|
-
)
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
password=True,
|
|
49
|
-
)
|
|
34
|
+
class EmptyApiKeyError(InitError):
|
|
35
|
+
"""Raised when API key is empty."""
|
|
50
36
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__("API key cannot be empty.")
|
|
54
39
|
|
|
55
|
-
llm_config = LLMConfig(
|
|
56
|
-
model=LLMProvider(llm_provider),
|
|
57
|
-
api_key=api_key,
|
|
58
|
-
)
|
|
59
40
|
|
|
60
|
-
|
|
41
|
+
def setup_project_name(force: bool = False) -> tuple[str, Path]:
|
|
42
|
+
"""Setup the project name."""
|
|
43
|
+
project_name = Prompt.ask("[bold]Enter your project name[/bold]")
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
if not project_name:
|
|
46
|
+
raise EmptyProjectNameError()
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
console.print(f"[bold green]✓[/bold green] Created project [cyan]{project_name}[/cyan]")
|
|
67
|
-
console.print(
|
|
68
|
-
f"[bold green]✓[/bold green] Created [dim]{project_path / 'nao_config.yaml'}[/dim]"
|
|
69
|
-
)
|
|
70
|
-
console.print()
|
|
71
|
-
console.print("[bold green]Done![/bold green] Your nao project is ready. 🎉")
|
|
48
|
+
project_path = Path(project_name)
|
|
72
49
|
|
|
50
|
+
if project_path.exists() and not force:
|
|
51
|
+
raise ProjectExistsError(project_name)
|
|
52
|
+
|
|
53
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
return project_name, project_path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def setup_bigquery() -> BigQueryConfig:
|
|
59
|
+
"""Setup a BigQuery database configuration."""
|
|
60
|
+
console.print("\n[bold cyan]BigQuery Configuration[/bold cyan]\n")
|
|
61
|
+
|
|
62
|
+
name = Prompt.ask("[bold]Connection name[/bold]", default="bigquery-prod")
|
|
63
|
+
|
|
64
|
+
project_id = Prompt.ask("[bold]GCP Project ID[/bold]")
|
|
65
|
+
if not project_id:
|
|
66
|
+
raise InitError("GCP Project ID cannot be empty.")
|
|
67
|
+
|
|
68
|
+
dataset_id = Prompt.ask("[bold]Default dataset[/bold] [dim](optional, press Enter to skip)[/dim]", default="")
|
|
69
|
+
|
|
70
|
+
credentials_path = Prompt.ask(
|
|
71
|
+
"[bold]Service account JSON path[/bold] [dim](optional, uses ADC if empty)[/dim]",
|
|
72
|
+
default="",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return BigQueryConfig(
|
|
76
|
+
name=name,
|
|
77
|
+
project_id=project_id,
|
|
78
|
+
dataset_id=dataset_id or None,
|
|
79
|
+
credentials_path=credentials_path or None,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def setup_databases() -> list[DatabaseConfig]:
|
|
84
|
+
"""Setup database configurations."""
|
|
85
|
+
databases: list[DatabaseConfig] = []
|
|
86
|
+
|
|
87
|
+
should_setup = Confirm.ask("\n[bold]Set up database connections?[/bold]", default=True)
|
|
88
|
+
|
|
89
|
+
if not should_setup:
|
|
90
|
+
return databases
|
|
91
|
+
|
|
92
|
+
while True:
|
|
93
|
+
console.print("\n[bold cyan]Database Configuration[/bold cyan]\n")
|
|
94
|
+
|
|
95
|
+
db_type_choices = [t.value for t in DatabaseType]
|
|
96
|
+
db_type = Prompt.ask(
|
|
97
|
+
"[bold]Select database type[/bold]",
|
|
98
|
+
choices=db_type_choices,
|
|
99
|
+
default=db_type_choices[0],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if db_type == DatabaseType.BIGQUERY.value:
|
|
103
|
+
db_config = setup_bigquery()
|
|
104
|
+
databases.append(db_config)
|
|
105
|
+
console.print(f"\n[bold green]✓[/bold green] Added database [cyan]{db_config.name}[/cyan]")
|
|
106
|
+
|
|
107
|
+
add_another = Confirm.ask("\n[bold]Add another database?[/bold]", default=False)
|
|
108
|
+
if not add_another:
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
return databases
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def setup_llm() -> LLMConfig | None:
|
|
115
|
+
"""Setup the LLM configuration."""
|
|
116
|
+
llm_config = None
|
|
117
|
+
should_setup = Confirm.ask("\n[bold]Set up LLM configuration?[/bold]", default=True)
|
|
118
|
+
|
|
119
|
+
if should_setup:
|
|
120
|
+
console.print("\n[bold cyan]LLM Configuration[/bold cyan]\n")
|
|
121
|
+
|
|
122
|
+
provider_choices = [p.value for p in LLMProvider]
|
|
123
|
+
llm_provider = Prompt.ask(
|
|
124
|
+
"[bold]Select LLM provider[/bold]",
|
|
125
|
+
choices=provider_choices,
|
|
126
|
+
default=provider_choices[0],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
api_key = Prompt.ask(
|
|
130
|
+
f"[bold]Enter your {llm_provider.upper()} API key[/bold]",
|
|
131
|
+
password=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not api_key:
|
|
135
|
+
raise EmptyApiKeyError()
|
|
136
|
+
|
|
137
|
+
llm_config = LLMConfig(
|
|
138
|
+
provider=LLMProvider(llm_provider),
|
|
139
|
+
api_key=api_key,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return llm_config
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def init(
|
|
146
|
+
*,
|
|
147
|
+
force: Annotated[bool, Parameter(name=["-f", "--force"])] = False,
|
|
148
|
+
):
|
|
149
|
+
"""Initialize a new nao project.
|
|
150
|
+
|
|
151
|
+
Creates a project folder with a nao_config.yaml configuration file.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
force : bool
|
|
156
|
+
Force re-initialization even if the folder already exists.
|
|
157
|
+
"""
|
|
158
|
+
console.print("\n[bold cyan]🚀 nao project initialization[/bold cyan]\n")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
project_name, project_path = setup_project_name(force=force)
|
|
162
|
+
config = NaoConfig(
|
|
163
|
+
project_name=project_name,
|
|
164
|
+
databases=setup_databases(),
|
|
165
|
+
llm=setup_llm(),
|
|
166
|
+
)
|
|
167
|
+
config.save(project_path)
|
|
168
|
+
|
|
169
|
+
console.print()
|
|
170
|
+
console.print(f"[bold green]✓[/bold green] Created project [cyan]{project_name}[/cyan]")
|
|
171
|
+
console.print(f"[bold green]✓[/bold green] Created [dim]{project_path / 'nao_config.yaml'}[/dim]")
|
|
172
|
+
console.print()
|
|
173
|
+
console.print("[bold green]Done![/bold green] Your nao project is ready. 🎉")
|
|
174
|
+
except InitError as e:
|
|
175
|
+
console.print(f"[bold red]✗[/bold red] {e}")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
|
6
|
+
|
|
7
|
+
from nao_core.config import NaoConfig
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_table_schema_markdown(conn, dataset: str, table: str) -> str:
|
|
13
|
+
"""Generate markdown content describing a table's columns."""
|
|
14
|
+
try:
|
|
15
|
+
# Get the table reference and its schema
|
|
16
|
+
full_table_name = f"{dataset}.{table}"
|
|
17
|
+
t = conn.table(full_table_name)
|
|
18
|
+
schema = t.schema()
|
|
19
|
+
|
|
20
|
+
lines = [
|
|
21
|
+
f"# {table}",
|
|
22
|
+
"",
|
|
23
|
+
f"**Dataset:** `{dataset}`",
|
|
24
|
+
"",
|
|
25
|
+
"## Columns",
|
|
26
|
+
"",
|
|
27
|
+
"| Column | Type | Nullable |",
|
|
28
|
+
"|--------|------|----------|",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
for name, dtype in schema.items():
|
|
32
|
+
nullable = "Yes" if dtype.nullable else "No"
|
|
33
|
+
lines.append(f"| `{name}` | `{dtype}` | {nullable} |")
|
|
34
|
+
|
|
35
|
+
return "\n".join(lines)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return f"# {table}\n\nError fetching schema: {e}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sync_bigquery(db_config, base_path: Path, progress: Progress) -> tuple[int, int]:
|
|
41
|
+
"""Sync BigQuery database schema to markdown files.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (datasets_synced, tables_synced)
|
|
45
|
+
"""
|
|
46
|
+
conn = db_config.connect()
|
|
47
|
+
db_path = base_path / "bigquery" / db_config.name
|
|
48
|
+
|
|
49
|
+
datasets_synced = 0
|
|
50
|
+
tables_synced = 0
|
|
51
|
+
|
|
52
|
+
# Get datasets to sync
|
|
53
|
+
if db_config.dataset_id:
|
|
54
|
+
datasets = [db_config.dataset_id]
|
|
55
|
+
else:
|
|
56
|
+
datasets = conn.list_databases()
|
|
57
|
+
|
|
58
|
+
dataset_task = progress.add_task(
|
|
59
|
+
f"[dim]{db_config.name}[/dim]",
|
|
60
|
+
total=len(datasets),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for dataset in datasets:
|
|
64
|
+
dataset_path = db_path / dataset
|
|
65
|
+
dataset_path.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
datasets_synced += 1
|
|
67
|
+
|
|
68
|
+
# List tables in this dataset
|
|
69
|
+
try:
|
|
70
|
+
tables = conn.list_tables(database=dataset)
|
|
71
|
+
except Exception:
|
|
72
|
+
progress.update(dataset_task, advance=1)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
table_task = progress.add_task(
|
|
76
|
+
f" [cyan]{dataset}[/cyan]",
|
|
77
|
+
total=len(tables),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
for table in tables:
|
|
81
|
+
table_path = dataset_path / table
|
|
82
|
+
table_path.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
columns_md = get_table_schema_markdown(conn, dataset, table)
|
|
85
|
+
columns_file = table_path / "columns.md"
|
|
86
|
+
columns_file.write_text(columns_md)
|
|
87
|
+
tables_synced += 1
|
|
88
|
+
|
|
89
|
+
progress.update(table_task, advance=1)
|
|
90
|
+
|
|
91
|
+
progress.update(dataset_task, advance=1)
|
|
92
|
+
|
|
93
|
+
return datasets_synced, tables_synced
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sync(output_dir: str = "databases"):
|
|
97
|
+
"""Sync database schemas to local markdown files.
|
|
98
|
+
|
|
99
|
+
Creates a folder structure with table schemas:
|
|
100
|
+
databases/bigquery/<connection>/<dataset>/<table>/columns.md
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
output_dir: Output directory for the database schemas (default: "databases")
|
|
104
|
+
"""
|
|
105
|
+
console.print("\n[bold cyan]🔄 nao sync[/bold cyan]\n")
|
|
106
|
+
|
|
107
|
+
# Load config
|
|
108
|
+
config = NaoConfig.try_load()
|
|
109
|
+
if not config:
|
|
110
|
+
console.print("[bold red]✗[/bold red] No nao_config.yaml found in current directory")
|
|
111
|
+
console.print("[dim]Run 'nao init' to create a configuration file[/dim]")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
console.print(f"[dim]Project:[/dim] {config.project_name}")
|
|
115
|
+
|
|
116
|
+
if not config.databases:
|
|
117
|
+
console.print("[dim]No databases configured[/dim]")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
base_path = Path(output_dir)
|
|
121
|
+
total_datasets = 0
|
|
122
|
+
total_tables = 0
|
|
123
|
+
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
with Progress(
|
|
127
|
+
SpinnerColumn(style="dim"),
|
|
128
|
+
TextColumn("[progress.description]{task.description}"),
|
|
129
|
+
BarColumn(bar_width=30, style="dim", complete_style="cyan", finished_style="green"),
|
|
130
|
+
TaskProgressColumn(),
|
|
131
|
+
console=console,
|
|
132
|
+
transient=False,
|
|
133
|
+
) as progress:
|
|
134
|
+
for db in config.databases:
|
|
135
|
+
try:
|
|
136
|
+
if db.type == "bigquery":
|
|
137
|
+
datasets, tables = sync_bigquery(db, base_path, progress)
|
|
138
|
+
total_datasets += datasets
|
|
139
|
+
total_tables += tables
|
|
140
|
+
else:
|
|
141
|
+
console.print(f"[yellow]⚠ Unsupported database type: {db.type}[/yellow]")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
console.print(f"[bold red]✗[/bold red] Failed to sync {db.name}: {e}")
|
|
144
|
+
|
|
145
|
+
console.print()
|
|
146
|
+
console.print(
|
|
147
|
+
f"[green]✓[/green] Synced [bold]{total_tables}[/bold] tables across [bold]{total_datasets}[/bold] datasets"
|
|
148
|
+
)
|
|
149
|
+
console.print(f"[dim] → {base_path.absolute()}[/dim]")
|
|
150
|
+
console.print()
|
nao_core/config.py
CHANGED
|
@@ -1,64 +1,136 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Literal
|
|
3
4
|
|
|
5
|
+
import ibis
|
|
4
6
|
import yaml
|
|
5
|
-
from
|
|
7
|
+
from ibis import BaseBackend
|
|
8
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class LLMProvider(str, Enum):
|
|
9
|
-
|
|
12
|
+
"""Supported LLM providers."""
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
OPENAI = "openai"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseType(str, Enum):
|
|
18
|
+
"""Supported database types."""
|
|
19
|
+
|
|
20
|
+
BIGQUERY = "bigquery"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BigQueryConfig(BaseModel):
|
|
24
|
+
"""BigQuery-specific configuration."""
|
|
25
|
+
|
|
26
|
+
type: Literal["bigquery"] = "bigquery"
|
|
27
|
+
name: str = Field(description="A friendly name for this connection")
|
|
28
|
+
project_id: str = Field(description="GCP project ID")
|
|
29
|
+
dataset_id: str | None = Field(default=None, description="Default BigQuery dataset")
|
|
30
|
+
credentials_path: str | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description="Path to service account JSON file. If not provided, uses Application Default Credentials (ADC)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def connect(self) -> BaseBackend:
|
|
36
|
+
"""Create an Ibis BigQuery connection."""
|
|
37
|
+
kwargs: dict = {"project_id": self.project_id}
|
|
38
|
+
|
|
39
|
+
if self.dataset_id:
|
|
40
|
+
kwargs["dataset_id"] = self.dataset_id
|
|
41
|
+
|
|
42
|
+
if self.credentials_path:
|
|
43
|
+
from google.oauth2 import service_account
|
|
44
|
+
|
|
45
|
+
credentials = service_account.Credentials.from_service_account_file(
|
|
46
|
+
self.credentials_path,
|
|
47
|
+
scopes=["https://www.googleapis.com/auth/bigquery"],
|
|
48
|
+
)
|
|
49
|
+
kwargs["credentials"] = credentials
|
|
50
|
+
|
|
51
|
+
return ibis.bigquery.connect(**kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
DatabaseConfig = BigQueryConfig
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_database_config(data: dict) -> DatabaseConfig:
|
|
58
|
+
"""Parse a database config dict into the appropriate type."""
|
|
59
|
+
db_type = data.get("type")
|
|
60
|
+
if db_type == "bigquery":
|
|
61
|
+
return BigQueryConfig.model_validate(data)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"Unknown database type: {db_type}")
|
|
12
64
|
|
|
13
65
|
|
|
14
66
|
class LLMConfig(BaseModel):
|
|
15
|
-
|
|
67
|
+
"""LLM configuration."""
|
|
68
|
+
|
|
69
|
+
provider: LLMProvider = Field(description="The LLM provider to use")
|
|
70
|
+
api_key: str = Field(description="The API key to use")
|
|
16
71
|
|
|
17
|
-
model: LLMProvider = Field(description="The LLM provider to use")
|
|
18
|
-
api_key: str = Field(description="The API key to use")
|
|
19
72
|
|
|
20
73
|
class NaoConfig(BaseModel):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
"""nao project configuration."""
|
|
75
|
+
|
|
76
|
+
project_name: str = Field(description="The name of the nao project")
|
|
77
|
+
databases: list[BigQueryConfig] = Field(description="The databases to use")
|
|
78
|
+
llm: LLMConfig | None = Field(default=None, description="The LLM configuration")
|
|
79
|
+
|
|
80
|
+
@model_validator(mode="before")
|
|
81
|
+
@classmethod
|
|
82
|
+
def parse_databases(cls, data: dict) -> dict:
|
|
83
|
+
"""Parse database configs into their specific types."""
|
|
84
|
+
if "databases" in data and isinstance(data["databases"], list):
|
|
85
|
+
data["databases"] = [parse_database_config(db) if isinstance(db, dict) else db for db in data["databases"]]
|
|
86
|
+
return data
|
|
87
|
+
|
|
88
|
+
def save(self, path: Path) -> None:
|
|
89
|
+
"""Save the configuration to a YAML file."""
|
|
90
|
+
config_file = path / "nao_config.yaml"
|
|
91
|
+
with config_file.open("w") as f:
|
|
92
|
+
yaml.dump(
|
|
93
|
+
self.model_dump(mode="json", by_alias=True),
|
|
94
|
+
f,
|
|
95
|
+
default_flow_style=False,
|
|
96
|
+
sort_keys=False,
|
|
97
|
+
allow_unicode=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def load(cls, path: Path) -> "NaoConfig":
|
|
102
|
+
"""Load the configuration from a YAML file."""
|
|
103
|
+
config_file = path / "nao_config.yaml"
|
|
104
|
+
with config_file.open() as f:
|
|
105
|
+
data = yaml.safe_load(f)
|
|
106
|
+
return cls.model_validate(data)
|
|
107
|
+
|
|
108
|
+
def get_connection(self, name: str) -> BaseBackend:
|
|
109
|
+
"""Get an Ibis connection by database name."""
|
|
110
|
+
for db in self.databases:
|
|
111
|
+
if db.name == name:
|
|
112
|
+
return db.connect()
|
|
113
|
+
raise ValueError(f"Database '{name}' not found in configuration")
|
|
114
|
+
|
|
115
|
+
def get_all_connections(self) -> dict[str, BaseBackend]:
|
|
116
|
+
"""Get all Ibis connections as a dict keyed by name."""
|
|
117
|
+
return {db.name: db.connect() for db in self.databases}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def try_load(cls, path: Path | None = None) -> "NaoConfig | None":
|
|
121
|
+
"""Try to load config from path, returns None if not found or invalid.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path: Directory containing nao_config.yaml. Defaults to current directory.
|
|
125
|
+
"""
|
|
126
|
+
if path is None:
|
|
127
|
+
path = Path.cwd()
|
|
128
|
+
try:
|
|
129
|
+
return cls.load(path)
|
|
130
|
+
except (FileNotFoundError, ValueError, yaml.YAMLError):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def json_schema(cls) -> dict:
|
|
135
|
+
"""Generate JSON schema for the configuration."""
|
|
136
|
+
return cls.model_json_schema()
|
nao_core/main.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from cyclopts import App
|
|
2
2
|
|
|
3
|
-
from nao_core.commands import chat, init
|
|
3
|
+
from nao_core.commands import chat, debug, init, sync
|
|
4
4
|
|
|
5
5
|
app = App()
|
|
6
6
|
|
|
7
7
|
app.command(chat)
|
|
8
|
+
app.command(debug)
|
|
8
9
|
app.command(init)
|
|
10
|
+
app.command(sync)
|
|
9
11
|
|
|
10
12
|
if __name__ == "__main__":
|
|
11
|
-
|
|
13
|
+
app()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nao-core
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: nao Core is your analytics context builder with the best chat interface.
|
|
5
|
+
Project-URL: Homepage, https://getnao.io
|
|
6
|
+
Project-URL: Repository, https://github.com/naolabs/chat
|
|
7
|
+
Author: nao Labs
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,analytics,chat
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cyclopts>=4.4.4
|
|
23
|
+
Requires-Dist: ibis-framework[bigquery]>=9.0.0
|
|
24
|
+
Requires-Dist: openai>=1.0.0
|
|
25
|
+
Requires-Dist: pydantic>=2.10.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
27
|
+
Requires-Dist: rich>=14.0.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# nao CLI
|
|
31
|
+
|
|
32
|
+
Command-line interface for nao chat.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install nao-core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
nao --help
|
|
44
|
+
Usage: nao COMMAND
|
|
45
|
+
|
|
46
|
+
╭─ Commands ────────────────────────────────────────────────────────────────╮
|
|
47
|
+
│ chat Start the nao chat UI. │
|
|
48
|
+
│ init Initialize a new nao project. │
|
|
49
|
+
│ --help (-h) Display this message and exit. │
|
|
50
|
+
│ --version Display application version. │
|
|
51
|
+
╰───────────────────────────────────────────────────────────────────────────╯
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Initialize a new nao project
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
nao init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This will create a new nao project in the current directory. It will prompt you for a project name and ask you if you want to set up an LLM configuration.
|
|
61
|
+
|
|
62
|
+
### Start the nao chat UI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
nao chat
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This will start the nao chat UI. It will open the chat interface in your browser at `http://localhost:5005`.
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
### Building the package
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd cli
|
|
76
|
+
python build.py --help
|
|
77
|
+
Usage: build.py [OPTIONS]
|
|
78
|
+
|
|
79
|
+
Build and package nao-core CLI.
|
|
80
|
+
|
|
81
|
+
╭─ Commands ────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
82
|
+
│ --help (-h) Display this message and exit. │
|
|
83
|
+
│ --version Display application version. │
|
|
84
|
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
85
|
+
╭─ Parameters ──────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
86
|
+
│ --force -f --no-force Force rebuild the server binary [default: False] │
|
|
87
|
+
│ --skip-server -s --no-skip-server Skip server build, only build Python package [default: False] │
|
|
88
|
+
│ --bump Bump version before building (patch, minor, major) [choices: patch, │
|
|
89
|
+
│ minor, major] │
|
|
90
|
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This will:
|
|
94
|
+
1. Build the frontend with Vite
|
|
95
|
+
2. Compile the backend with Bun into a standalone binary
|
|
96
|
+
3. Bundle everything into a Python wheel in `dist/`
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
- `--force` / `-f`: Force rebuild the server binary
|
|
100
|
+
- `--skip-server`: Skip server build, only build Python package
|
|
101
|
+
- `--bump`: Bump version before building (patch, minor, major)
|
|
102
|
+
|
|
103
|
+
### Installing for development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd cli
|
|
107
|
+
pip install -e .
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Publishing to PyPI
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Build first
|
|
114
|
+
python build.py
|
|
115
|
+
|
|
116
|
+
# Publish
|
|
117
|
+
uv publish dist/*
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Architecture
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
nao chat (CLI command)
|
|
124
|
+
↓ spawns
|
|
125
|
+
nao-chat-server (Bun-compiled binary)
|
|
126
|
+
↓ serves
|
|
127
|
+
Backend API + Frontend Static Files
|
|
128
|
+
↓
|
|
129
|
+
Browser at http://localhost:5005
|
|
130
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
nao_core/__init__.py,sha256=OTB7vGhVCdxrovMSyxjPvmW5myiN0jPFano1PGXh6Dc,46
|
|
2
|
+
nao_core/config.py,sha256=uNZ_q6f6IC3i7e8SFu-CXJhcCboNNm3UjnL4KHpw-cw,4527
|
|
3
|
+
nao_core/main.py,sha256=f00vLL4s2B2kCMa8y3lI56LX3TnUppWzYmqM6eOGomA,205
|
|
4
|
+
nao_core/bin/db.sqlite,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
nao_core/bin/nao-chat-server,sha256=zqD16vlL_FKNOrbrRyvaEJGpfwfIOtWgLkvdgfxDP44,63220848
|
|
6
|
+
nao_core/bin/public/favicon.ico,sha256=PRD32mxgMXg0AIFmjErFs66XQ8qaJiqw_NMS-7n0i90,3870
|
|
7
|
+
nao_core/bin/public/index.html,sha256=NTU3lowrcs_NGlGdrPulJb7k9vmfbHTm55HKk9SfVV0,673
|
|
8
|
+
nao_core/bin/public/assets/index-BSxC58nD.js,sha256=KfT52WftnDWjgEEplssDzkzOPgCR_1jaXnczs_Gn3bk,178296
|
|
9
|
+
nao_core/bin/public/assets/index-Dh3br3Ia.js,sha256=UlskObLoJLwNKErNk6DrjrBdsbemOJ9BT16_ejD5234,333150
|
|
10
|
+
nao_core/bin/public/assets/index-heKLHGGE.css,sha256=9tGsJsaCXrJ0-GQptxyrmuQhX4rWcUNLvWmR8n0-5vY,30119
|
|
11
|
+
nao_core/commands/__init__.py,sha256=rFCuUyA9wM4hS6CxCt2M_5W4VysyQXh6HkA_hyRCxPc,207
|
|
12
|
+
nao_core/commands/chat.py,sha256=xhcBfP40IedunfLX73VNDM5VOGBDnqqii4khoVE-imY,4430
|
|
13
|
+
nao_core/commands/debug.py,sha256=Hdtb6_2F-a9Nr-aU_exdnPZY0-Lf9dgV1QX6QxPr9P8,4794
|
|
14
|
+
nao_core/commands/init.py,sha256=lL4Zn8KIhgH2hRTFOBuQIHpiReSeOF517J4s4zBdwEk,5175
|
|
15
|
+
nao_core/commands/sync.py,sha256=P_AHbTwIifmvhvAJuYR3zA9gdBD2QbaONHQArzS4nXc,4654
|
|
16
|
+
nao_core-0.0.11.dist-info/METADATA,sha256=XWK2ujf5mNjTE0aEjG2FYforbf8nE8PI6GtwLtbYiaI,5143
|
|
17
|
+
nao_core-0.0.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
nao_core-0.0.11.dist-info/entry_points.txt,sha256=SZakIiNybgS3pl_OEZVLyLSweadeBFoEMBECMoj9czY,42
|
|
19
|
+
nao_core-0.0.11.dist-info/licenses/LICENSE,sha256=rn5YtWB6E5hPQI49tCTNSyqlArWGsB6HzA5FfSbRHRs,1066
|
|
20
|
+
nao_core-0.0.11.dist-info/RECORD,,
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: nao-core
|
|
3
|
-
Version: 0.0.9
|
|
4
|
-
Summary: nao Core is your analytics context builder with the best chat interface.
|
|
5
|
-
Project-URL: Homepage, https://github.com/naolabs/chat
|
|
6
|
-
Project-URL: Repository, https://github.com/naolabs/chat
|
|
7
|
-
Author: nao Labs
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Keywords: ai,analytics,chat
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Environment :: Console
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Operating System :: MacOS
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Requires-Python: >=3.10
|
|
22
|
-
Requires-Dist: cyclopts>=4.4.4
|
|
23
|
-
Requires-Dist: pydantic>=2.10.0
|
|
24
|
-
Requires-Dist: pyyaml>=6.0.0
|
|
25
|
-
Requires-Dist: rich>=14.0.0
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
|
|
28
|
-
# nao CLI
|
|
29
|
-
|
|
30
|
-
Command-line interface for nao chat.
|
|
31
|
-
|
|
32
|
-
## Installation
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pip install nao-core
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Usage
|
|
39
|
-
|
|
40
|
-
### Start the chat interface
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
nao chat
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This will start the nao chat server and open the web interface in your browser at `http://localhost:5005`.
|
|
47
|
-
|
|
48
|
-
## Development
|
|
49
|
-
|
|
50
|
-
### Building the package
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
cd cli
|
|
54
|
-
python build.py
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
This will:
|
|
58
|
-
1. Build the frontend with Vite
|
|
59
|
-
2. Compile the backend with Bun into a standalone binary
|
|
60
|
-
3. Bundle everything into a Python wheel in `dist/`
|
|
61
|
-
|
|
62
|
-
Options:
|
|
63
|
-
- `--force` / `-f`: Force rebuild the server binary
|
|
64
|
-
- `--skip-server`: Skip server build, only build Python package
|
|
65
|
-
|
|
66
|
-
### Installing for development
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
cd cli
|
|
70
|
-
pip install -e .
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Publishing to PyPI
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
# Build first
|
|
77
|
-
python build.py
|
|
78
|
-
|
|
79
|
-
# Publish
|
|
80
|
-
uv publish dist/*
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Architecture
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
nao chat (CLI command)
|
|
87
|
-
↓ spawns
|
|
88
|
-
nao-chat-server (Bun-compiled binary)
|
|
89
|
-
↓ serves
|
|
90
|
-
Backend API + Frontend Static Files
|
|
91
|
-
↓
|
|
92
|
-
Browser at http://localhost:5005
|
|
93
|
-
```
|
nao_core-0.0.9.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
nao_core/__init__.py,sha256=u4cYroaAmhjQLYOiejO2wTSrfGh4JcAAqucY_38BWw4,46
|
|
2
|
-
nao_core/config.py,sha256=O6H-qnsv6cLeNdyMmI4gBivLk3mlbJGM2xaF6kMgOlE,1644
|
|
3
|
-
nao_core/main.py,sha256=4RbHIM9lZOZoZKAJhibPGUI_ZaE1FaBgN4YDhgF2mpU,152
|
|
4
|
-
nao_core/bin/db.sqlite,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
nao_core/bin/nao-chat-server,sha256=zqD16vlL_FKNOrbrRyvaEJGpfwfIOtWgLkvdgfxDP44,63220848
|
|
6
|
-
nao_core/bin/public/favicon.ico,sha256=PRD32mxgMXg0AIFmjErFs66XQ8qaJiqw_NMS-7n0i90,3870
|
|
7
|
-
nao_core/bin/public/index.html,sha256=NTU3lowrcs_NGlGdrPulJb7k9vmfbHTm55HKk9SfVV0,673
|
|
8
|
-
nao_core/bin/public/assets/index-BSxC58nD.js,sha256=KfT52WftnDWjgEEplssDzkzOPgCR_1jaXnczs_Gn3bk,178296
|
|
9
|
-
nao_core/bin/public/assets/index-Dh3br3Ia.js,sha256=UlskObLoJLwNKErNk6DrjrBdsbemOJ9BT16_ejD5234,333150
|
|
10
|
-
nao_core/bin/public/assets/index-heKLHGGE.css,sha256=9tGsJsaCXrJ0-GQptxyrmuQhX4rWcUNLvWmR8n0-5vY,30119
|
|
11
|
-
nao_core/commands/__init__.py,sha256=d6Aq8CSfgpfsFkyT2i7IywC1guB853UH5RYw2MvjelE,109
|
|
12
|
-
nao_core/commands/chat.py,sha256=iuG2R_v3XBg04AEjUDyxSQbOaznRmWWujXe4FNVBKOo,3856
|
|
13
|
-
nao_core/commands/init.py,sha256=howdDyAAZ-Ox3Betmqk6C2TfuUBWEwkEUk-Nm40gcTY,1866
|
|
14
|
-
nao_core-0.0.9.dist-info/METADATA,sha256=6khBkxOY8G745XqHHJwplPtvX721t3oY9Ve1tMRp9dU,1971
|
|
15
|
-
nao_core-0.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
-
nao_core-0.0.9.dist-info/entry_points.txt,sha256=SZakIiNybgS3pl_OEZVLyLSweadeBFoEMBECMoj9czY,42
|
|
17
|
-
nao_core-0.0.9.dist-info/licenses/LICENSE,sha256=rn5YtWB6E5hPQI49tCTNSyqlArWGsB6HzA5FfSbRHRs,1066
|
|
18
|
-
nao_core-0.0.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|