Skip to content

Commit 3d79a0a

Browse files
bowenislandsongbowenislandsong
authored andcommitted
Add registries workflows models installer and tests
1 parent 0256325 commit 3d79a0a

17 files changed

Lines changed: 1263 additions & 83 deletions

README.md

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@ Start here:
1818
- [Example Screenshots](docs/example-screenshots.md)
1919
- [GitHub Pages Deploy](docs/github-pages.md)
2020

21+
## Quick Install
22+
23+
Install the `agentic-loop` command on Linux or macOS with the bootstrap script:
24+
25+
```bash
26+
curl -fsSL https://raw.githubusercontent.com/Bowen-AI/AgenticLocal/main/scripts/install.sh \
27+
-o install-agentic-loop.sh
28+
bash install-agentic-loop.sh --with-ollama
29+
agentic-loop --version
30+
```
31+
32+
Or run it directly:
33+
34+
```bash
35+
curl -fsSL https://raw.githubusercontent.com/Bowen-AI/AgenticLocal/main/scripts/install.sh \
36+
| bash -s -- --with-ollama
37+
```
38+
39+
From a local checkout, use the same installer:
40+
41+
```bash
42+
scripts/install.sh
43+
agentic-loop --version
44+
```
45+
46+
The installer creates an isolated environment under
47+
`~/.local/share/agentic-loop`, writes a launcher to `~/.local/bin`, prompts for
48+
Ollama when it is missing, and pulls the default `qwen3.5:9b` model when
49+
Ollama is available. Use `--no-model-pull` to skip the model download.
50+
51+
Common install modes:
52+
53+
```bash
54+
scripts/install.sh --with-ollama
55+
scripts/install.sh --ollama-model qwen3.5:9b
56+
scripts/install.sh --no-ollama
57+
```
58+
2159
## Run The Demo
2260

2361
Start an interactive agent chat:
@@ -26,7 +64,9 @@ Start an interactive agent chat:
2664
python3 -m agentic_loop chat
2765
```
2866

29-
Start interactive chat with opt-in public web/news tools:
67+
Interactive chat defaults to Ollama with `qwen3.5:9b`; run `ollama pull qwen3.5:9b`
68+
once if the model is not installed yet. Start it with opt-in public web/news
69+
tools:
3070

3171
```bash
3272
python3 -m agentic_loop chat --enable-network-tools
@@ -84,17 +124,18 @@ Start the local HTTP agent service:
84124
python3 -m agentic_loop serve --host 127.0.0.1 --port 8765
85125
```
86126

87-
The service starts with a default provider, but clients can choose provider and
88-
model per request or per session. The default is the dependency-free `rule`
89-
provider, so Ollama and API keys are optional.
127+
The service defaults to Ollama with `qwen3.5:9b`, but clients can choose provider and
128+
model per request or per session. Use `--provider rule` when you want the
129+
dependency-free deterministic provider.
90130

91-
Expose public web/news tools in the local service:
131+
Public web/news/fetch tools are enabled by default for the local service. Start
132+
without those network tools when you want a narrower server:
92133

93134
```bash
94135
python3 -m agentic_loop serve \
95136
--host 127.0.0.1 \
96137
--port 8765 \
97-
--enable-network-tools
138+
--disable-network-tools
98139
```
99140

100141
Then open the embedded voice mode:
@@ -153,11 +194,16 @@ Rules and workflows can be listed from the CLI:
153194
```bash
154195
python3 -m agentic_loop --rules
155196
python3 -m agentic_loop --workflows
197+
python3 -m agentic_loop --models
156198
python3 -m agentic_loop --workflow loop "Inspect data/sample.csv"
157199
python3 -m agentic_loop chat --enable-network-tools
158200
```
159201

160-
Inside chat, use `/rules`, `/rule on max_effort`, `/loop`, and `/search`.
202+
Inside chat, use `/rules`, `/rule on max_effort`, `/models`,
203+
`/model ollama qwen3.5:9b`, `/loop`, `/search`, and `/release`.
204+
205+
When chat starts without `--provider`, it uses `--provider ollama --model qwen3.5:9b`.
206+
Use `--provider rule` for deterministic offline demos and tool-loop tests.
161207

162208
## Verification
163209

@@ -251,6 +297,17 @@ The installer prefers an isolated venv under `~/.local/share/agentic-loop` and
251297
creates a launcher in `~/.local/bin`. If your system Python lacks venv/pip
252298
support, it prints the platform package to install.
253299

300+
Ollama is a system application, not a Python wheel dependency. The installer
301+
will prompt to install Ollama when it is missing and pulls the default model
302+
when Ollama is available:
303+
304+
```bash
305+
scripts/install.sh --with-ollama
306+
scripts/install.sh --ollama-model qwen3.5:9b
307+
scripts/install.sh --no-model-pull
308+
scripts/install.sh --no-ollama
309+
```
310+
254311
Build release artifacts:
255312

256313
```bash
@@ -270,7 +327,7 @@ agentic-loop "Inspect data/sample.csv as a dataset."
270327

271328
CI runs `scripts/check_release.sh` on Linux and macOS for Python 3.11 and 3.12.
272329

273-
## Code Map
330+
## Components
274331

275332
```text
276333
agentic_loop/
@@ -295,11 +352,32 @@ agentic_loop/
295352
factory.py # controller construction
296353
```
297354

355+
## System Diagram
356+
357+
The main runtime shape is:
358+
359+
```text
360+
User / UI
361+
-> CLI, terminal chat, HTTP API, browser voice page
362+
-> Agent Runtime
363+
-> Context Builder
364+
-> Model Adapter
365+
-> Rule Resolver
366+
-> Workspace Policy
367+
-> Tool Registry
368+
-> Event Logger
369+
-> Evaluator
370+
-> SQLite Storage
371+
-> sessions, messages, memory, events, traces, registries
372+
-> Workspace Files
373+
-> read roots, write roots, approval-gated roots
374+
```
375+
298376
## Architecture Direction
299377

300378
See [Architecture Decision](docs/architecture-decision.md).
301379

302-
Current architecture:
380+
Detailed architecture:
303381

304382
```text
305383
User / UI

agentic_loop/chat.py

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import argparse
22

33
from .factory import create_controller
4+
from .model_selection import (
5+
DEFAULT_INTERACTIVE_MODEL,
6+
DEFAULT_INTERACTIVE_PROVIDER,
7+
DEFAULT_OLLAMA_HOST,
8+
ModelSelection,
9+
parse_model_command,
10+
provider_registry,
11+
render_model_selection,
12+
)
413
from .session import AgentSession
514

615

@@ -10,10 +19,15 @@
1019
/rules Show rule toggles.
1120
/rule on KEY
1221
/rule off KEY
22+
/models Show model providers.
23+
/model Show current model.
24+
/model PROVIDER MODEL
25+
/model provider=PROVIDER model=MODEL [api_base=URL] [api_key=KEY]
1326
/workflows Show workflow presets.
1427
/workflow KEY
1528
/loop [goal]
1629
/search [query]
30+
/release [goal]
1731
/history Show the current session transcript.
1832
/exit Quit.
1933
"""
@@ -29,10 +43,10 @@ def run_chat(argv=None) -> int:
2943
parser.add_argument(
3044
"--provider",
3145
choices=["rule", "ollama", "openai", "openai-compatible", "gemini", "localai"],
32-
default="rule",
46+
default=DEFAULT_INTERACTIVE_PROVIDER,
3347
)
3448
parser.add_argument("--model", default=None)
35-
parser.add_argument("--ollama-host", default="http://127.0.0.1:11434")
49+
parser.add_argument("--ollama-host", default=DEFAULT_OLLAMA_HOST)
3650
parser.add_argument("--api-base", default=None)
3751
parser.add_argument("--api-key", default=None)
3852
parser.add_argument("--write-root", action="append", default=[])
@@ -48,29 +62,46 @@ def run_chat(argv=None) -> int:
4862
args = parser.parse_args(argv)
4963

5064
write_roots = ["outputs", *args.write_root]
51-
controller = create_controller(
52-
workspace=args.workspace,
53-
memory_path=args.memory,
54-
trace_path=args.trace,
55-
db_path=args.db,
56-
max_steps=args.max_steps,
65+
model_name = args.model
66+
if args.provider == DEFAULT_INTERACTIVE_PROVIDER and model_name is None:
67+
model_name = DEFAULT_INTERACTIVE_MODEL
68+
current_model = ModelSelection.from_values(
5769
provider=args.provider,
58-
model_name=args.model,
70+
model_name=model_name,
5971
ollama_host=args.ollama_host,
6072
api_base=args.api_base,
6173
api_key=args.api_key,
62-
write_roots=write_roots,
63-
approval_required_roots=args.approval_root,
64-
enable_network_tools=args.enable_network_tools,
65-
enabled_rule_keys=args.rule,
66-
disabled_rule_keys=args.no_rule,
67-
workflow_key=args.workflow,
6874
)
75+
76+
def make_controller(model_selection: ModelSelection):
77+
return create_controller(
78+
workspace=args.workspace,
79+
memory_path=args.memory,
80+
trace_path=args.trace,
81+
db_path=args.db,
82+
max_steps=args.max_steps,
83+
provider=model_selection.provider,
84+
model_name=model_selection.model_name,
85+
ollama_host=model_selection.ollama_host or args.ollama_host,
86+
api_base=model_selection.api_base,
87+
api_key=model_selection.api_key,
88+
write_roots=write_roots,
89+
approval_required_roots=args.approval_root,
90+
enable_network_tools=args.enable_network_tools,
91+
enabled_rule_keys=args.rule,
92+
disabled_rule_keys=args.no_rule,
93+
workflow_key=args.workflow,
94+
)
95+
96+
controller = make_controller(current_model)
6997
session = AgentSession(controller)
7098
current_workflow = args.workflow
7199

72100
print("agentic-loop chat")
73101
print("Type /help for commands, /exit to quit.")
102+
print(f"model: {render_model_selection(current_model)}")
103+
if current_model.provider == "rule":
104+
print("rule mode is deterministic/offline. Use /models and /model PROVIDER MODEL for a real chat model.")
74105

75106
while True:
76107
try:
@@ -92,6 +123,29 @@ def run_chat(argv=None) -> int:
92123
if user_input == "/tools":
93124
print(", ".join(sorted(controller.tools.names())))
94125
continue
126+
if user_input == "/models":
127+
_print_models()
128+
continue
129+
if user_input == "/model":
130+
print(f"model: {render_model_selection(current_model)}")
131+
continue
132+
if user_input.startswith("/model "):
133+
requested = user_input.split(maxsplit=1)[1].strip()
134+
try:
135+
current_model = parse_model_command(requested, fallback=current_model)
136+
old_history = session.history
137+
controller = make_controller(current_model)
138+
session = AgentSession(
139+
controller,
140+
history=old_history,
141+
session_id=session.session_id,
142+
storage=session.storage,
143+
)
144+
except ValueError as exc:
145+
print(f"model error: {exc}")
146+
continue
147+
print(f"model: {render_model_selection(current_model)}")
148+
continue
95149
if user_input == "/rules":
96150
_print_rules(controller)
97151
continue
@@ -114,35 +168,34 @@ def run_chat(argv=None) -> int:
114168
current_workflow = workflow.key
115169
print(f"workflow active: {workflow.command}")
116170
continue
117-
if user_input == "/loop" or user_input.startswith("/loop "):
118-
rest = user_input[len("/loop"):].strip()
119-
if rest:
120-
result = session.ask(rest, workflow_key="loop")
121-
print(result.final_answer)
122-
else:
123-
current_workflow = "loop"
124-
print("workflow active: /loop")
125-
continue
126-
if user_input == "/search" or user_input.startswith("/search "):
127-
rest = user_input[len("/search"):].strip()
171+
shortcut = _workflow_shortcut(controller, user_input)
172+
if shortcut is not None:
173+
workflow, rest = shortcut
128174
if rest:
129-
result = session.ask(rest, workflow_key="search")
130-
print(result.final_answer)
175+
_ask_and_print(session, rest, workflow_key=workflow.key)
131176
else:
132-
current_workflow = "search"
133-
print("workflow active: /search")
177+
current_workflow = workflow.key
178+
print(f"workflow active: {workflow.command}")
134179
continue
135180
if user_input == "/history":
136181
for item in session.transcript():
137182
print(f"{item['role']}: {item['content']}")
138183
continue
139184

140-
result = session.ask(user_input, workflow_key=current_workflow)
141-
print(result.final_answer)
185+
_ask_and_print(session, user_input, workflow_key=current_workflow)
142186

143187
return 0
144188

145189

190+
def _ask_and_print(session: AgentSession, message: str, workflow_key: str | None = None) -> None:
191+
try:
192+
result = session.ask(message, workflow_key=workflow_key)
193+
except Exception as exc:
194+
print(f"model error: {exc}")
195+
return
196+
print(result.final_answer)
197+
198+
146199
def _print_rules(controller) -> None:
147200
for rule in controller.rule_resolver.rules_with_state():
148201
status = "on" if rule["enabled"] else "off"
@@ -154,6 +207,24 @@ def _print_workflows(controller) -> None:
154207
print(f"{workflow.command} - {workflow.description}")
155208

156209

210+
def _workflow_shortcut(controller, user_input: str):
211+
command = user_input.split(maxsplit=1)[0]
212+
if not command.startswith("/"):
213+
return None
214+
workflow = controller.rule_resolver.workflow(command)
215+
if workflow is None or workflow.command != command:
216+
return None
217+
rest = user_input[len(command):].strip()
218+
return workflow, rest
219+
220+
221+
def _print_models() -> None:
222+
for item in provider_registry():
223+
required = "model required" if item.get("model_required") else "model optional"
224+
api_base = "api_base required" if item.get("api_base_required") else "api_base optional"
225+
print(f"{item['provider']}: {required}, {api_base} - {item['description']}")
226+
227+
157228
def _handle_rule_command(controller, user_input: str) -> None:
158229
parts = user_input.split()
159230
if len(parts) != 3 or parts[1] not in {"on", "off"}:

agentic_loop/model.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,16 +296,18 @@ def _final_from_tool(self, goal: str, tool_message: Message) -> str:
296296
return f"Tool {tool_message.name} completed."
297297

298298
def _direct_answer(self, goal: str, available_tools: set[str]) -> str:
299-
if goal in {"hello", "hi", "hey", "yo"}:
299+
if goal in {"hello", "hi", "hey", "yo"} or self._mentions(goal, "hello", "hi"):
300300
return (
301-
"Hi. I can use tools for files, CSVs, memory, and date/time. "
302-
"If you started chat with --enable-network-tools, I can also search web/news and fetch URLs."
301+
"Hi. I am running the deterministic rule provider, which is for tools, tests, "
302+
"and offline demos rather than open-ended chat. Use a model provider such as "
303+
"ollama, openai, openai-compatible, gemini, or localai for general conversation."
303304
)
304305
if goal in {"what", "??", "?", "help"}:
305306
tool_names = ", ".join(sorted(available_tools))
306307
return f"Try asking me to use a tool. Available tools: {tool_names}."
307308
return (
308-
"I did not recognize a tool-oriented request. Try: "
309+
"I am in deterministic rule mode, not a general chat model. Select a model provider "
310+
"for open-ended conversation, or try a tool-oriented request like: "
309311
"'list files', 'inspect data/sample.csv', 'what is today's date', "
310312
"'search news about AI', or 'fetch https://example.com'."
311313
)

0 commit comments

Comments
 (0)