lemonpie-cli 0.2.0a1__tar.gz → 0.2.0b1__tar.gz
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.
- lemonpie_cli-0.2.0a1/.github/workflows/publish.yml → lemonpie_cli-0.2.0b1/.github/workflows/ci.yml +3 -2
- lemonpie_cli-0.2.0b1/.github/workflows/release.yml +65 -0
- lemonpie_cli-0.2.0b1/.release-please/config.json +14 -0
- lemonpie_cli-0.2.0b1/.release-please/manifest.json +3 -0
- lemonpie_cli-0.2.0b1/CHANGELOG.md +42 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/PKG-INFO +1 -1
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/pyproject.toml +7 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/_version.py +2 -2
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/cli/config_cmd.py +8 -3
- lemonpie_cli-0.2.0a1/src/lemonpie/cli_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/cli/parser.py +1 -21
- lemonpie_cli-0.2.0a1/src/lemonpie/spinner.py → lemonpie_cli-0.2.0b1/src/lemonpie/cli/ui.py +31 -3
- lemonpie_cli-0.2.0b1/src/lemonpie/engine/__init__.py +0 -0
- lemonpie_cli-0.2.0a1/src/lemonpie/model_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/engine/models.py +29 -30
- lemonpie_cli-0.2.0b1/src/lemonpie/engine/ollama.py +49 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/main.py +43 -81
- lemonpie_cli-0.2.0b1/src/lemonpie/sessions/__init__.py +0 -0
- lemonpie_cli-0.2.0a1/src/lemonpie/session_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/sessions/storage.py +10 -19
- lemonpie_cli-0.2.0b1/tests/conftest.py +84 -0
- lemonpie_cli-0.2.0b1/tests/test_cli.py +110 -0
- lemonpie_cli-0.2.0b1/tests/test_sessions.py +294 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/uv.lock +69 -0
- lemonpie_cli-0.2.0a1/.github/workflows/release.yml +0 -43
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/.gitignore +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/LICENSE +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/README.md +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/config.example.json +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/docs/design/state-machine.md +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/__init__.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/cli/__init__.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/config.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/paths.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/time_utils.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Release & Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release-please:
|
|
15
|
+
name: Manage Release PR & Tags
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
outputs:
|
|
18
|
+
release_created: ${{ steps.release.outputs.releases_created }}
|
|
19
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: googleapis/release-please-action@v4
|
|
22
|
+
id: release
|
|
23
|
+
with:
|
|
24
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
25
|
+
config-file: .release-please/config.json
|
|
26
|
+
manifest-file: .release-please/manifest.json
|
|
27
|
+
|
|
28
|
+
pypi-publish:
|
|
29
|
+
name: Build and publish to PyPI
|
|
30
|
+
needs: release-please
|
|
31
|
+
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write # Required for Trusted Publisher OIDC authentication
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Check out repository
|
|
39
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0 # Required for hatch-vcs to inspect git history & tags
|
|
42
|
+
|
|
43
|
+
- name: Verify tag is reachable from main
|
|
44
|
+
run: |
|
|
45
|
+
git fetch origin main
|
|
46
|
+
if git merge-base --is-ancestor "${{ github.sha }}" origin/main; then
|
|
47
|
+
echo "Commit ${{ github.sha }} is on main. Proceeding."
|
|
48
|
+
else
|
|
49
|
+
echo "::error::Commit ${{ github.sha }} is not reachable from origin/main. Refusing to publish."
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
- name: Install uv
|
|
54
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
55
|
+
with:
|
|
56
|
+
version: "0.12.17"
|
|
57
|
+
|
|
58
|
+
- name: Set up Python
|
|
59
|
+
run: uv python install
|
|
60
|
+
|
|
61
|
+
- name: Build package artifacts
|
|
62
|
+
run: uv build
|
|
63
|
+
|
|
64
|
+
- name: Publish to PyPI
|
|
65
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release-type": "python",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"package-name": "lemonpie-cli",
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"bump-minor-pre-major": true,
|
|
8
|
+
"bump-patch-for-minor-pre-major": true,
|
|
9
|
+
"prerelease": true,
|
|
10
|
+
"versioning": "prerelease",
|
|
11
|
+
"include-component-in-tag": false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0-beta.1](https://github.com/famousStratum/LemonPie/compare/v0.2.0-alpha.3...v0.2.0-beta.1) (2026-09-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* eliminate now_ts() double-calls and session ID collisions in session creation ([#27](https://github.com/famousStratum/LemonPie/issues/27)) ([7864136](https://github.com/famousStratum/LemonPie/commit/7864136b6dc200e1e682b61252eadc74e99ac66a))
|
|
9
|
+
|
|
10
|
+
## [0.2.0-alpha.3](https://github.com/famousStratum/LemonPie/compare/v0.2.0-alpha.2...v0.2.0-alpha.3) (2026-09-21)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* resolve tuple/dict mismatch in model alias lookup and dead code in session loading ([#23](https://github.com/famousStratum/LemonPie/issues/23)) ([65152bf](https://github.com/famousStratum/LemonPie/commit/65152bf9d6df0d91addad5aa72f73145383b99d9))
|
|
16
|
+
|
|
17
|
+
## [0.2.0-alpha.2](https://github.com/famousStratum/LemonPie/compare/lemonpie-cli-v0.2.0-alpha.1...lemonpie-cli-v0.2.0-alpha.2) (2026-09-21)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **cli:** enhance error handling, model defaults, timeouts, and connection status ([9147dc0](https://github.com/famousStratum/LemonPie/commit/9147dc0293065f0b2227ac8f36cdacf0502ca7c4))
|
|
23
|
+
* **cli:** improve connection error handling, timeouts, and documentation ([83e2659](https://github.com/famousStratum/LemonPie/commit/83e2659182717d82feb28be4b85d14e85679d751))
|
|
24
|
+
* **paths:** Depend on platformdirs for config/session paths; bump requires-python to >=3.11 ([6fbf8e9](https://github.com/famousStratum/LemonPie/commit/6fbf8e92d27cbec20c57f404aecf99940329b238))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **build:** explicitly specify hatchling wheel package directory ([705980a](https://github.com/famousStratum/LemonPie/commit/705980a9eda1672829119db0fcd2aab4a9c04101))
|
|
30
|
+
* **build:** specify hatchling package target ([b759f2d](https://github.com/famousStratum/LemonPie/commit/b759f2d086bccb7b39d456844d1bb7403410e8d4))
|
|
31
|
+
* **cli:** import missing resolve_session in main entrypoint ([f5c4c5a](https://github.com/famousStratum/LemonPie/commit/f5c4c5ae1cc7ade21ba41eb92a9ecc640a7fe969))
|
|
32
|
+
* **cli:** reorder main entrypoint control flow and fix argument handling ([3a182d2](https://github.com/famousStratum/LemonPie/commit/3a182d2485af36f8a98a28cb41459f968fc9f4d4))
|
|
33
|
+
* **cli:** reorder main entrypoint control flow and resolve model assignment ([ad944f9](https://github.com/famousStratum/LemonPie/commit/ad944f93e2661eb3f2cc1abfe6cdeedc04f6ee86))
|
|
34
|
+
* **cli:** resolve NameError in main entrypoint ([532d661](https://github.com/famousStratum/LemonPie/commit/532d6617ff589de5f5def690e6390b1341517f84))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
* Add roadmap section to README and initial state-machine design doc ([fa9b000](https://github.com/famousStratum/LemonPie/commit/fa9b0009aaca85cbcd3e5a6f1e1c6c4d3bc40196))
|
|
40
|
+
* update README for src/ layout and platformdirs ([a15cde2](https://github.com/famousStratum/LemonPie/commit/a15cde25a08c49d11366bc64c31aa80da9590838))
|
|
41
|
+
* update README.md ([cf7a046](https://github.com/famousStratum/LemonPie/commit/cf7a0468474cc6bb47dbf33148a6b18d11b44fdf))
|
|
42
|
+
* update roadmap in README.md ([d0a3316](https://github.com/famousStratum/LemonPie/commit/d0a33167361efd400682f54780aaf3fff738e41d))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lemonpie-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0b1
|
|
4
4
|
Summary: LemonPie — A lightweight Ollama client for terminal users
|
|
5
5
|
Project-URL: homepage, https://versicode.com/lemonpie
|
|
6
6
|
Project-URL: repository, https://github.com/famousStratum/LemonPie
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0b1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'b1')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,8 +15,8 @@ import argparse, json, os, sys
|
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
17
17
|
from lemonpie.paths import CONFIG_FILE
|
|
18
|
-
from lemonpie.
|
|
19
|
-
from lemonpie.
|
|
18
|
+
from lemonpie.cli.ui import confirm
|
|
19
|
+
from lemonpie.engine.models import (
|
|
20
20
|
find_by_alias,
|
|
21
21
|
find_by_name,
|
|
22
22
|
)
|
|
@@ -196,7 +196,7 @@ def cmd_show(cfg):
|
|
|
196
196
|
return 0
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
def
|
|
199
|
+
def build_parser():
|
|
200
200
|
parser = argparse.ArgumentParser(prog="lmConfig", description="Manage LemonPie configuration (config.json)")
|
|
201
201
|
parser.add_argument("--host", help="Set Ollama host URL (http(s)://host:port)")
|
|
202
202
|
parser.add_argument("--default-model", help="Set default model using alias or full name (must exist in models)")
|
|
@@ -205,6 +205,11 @@ def main():
|
|
|
205
205
|
parser.add_argument("--list-models", action="store_true", help="List configured model aliases and names")
|
|
206
206
|
parser.add_argument("--show", action="store_true", help="Print full config.json")
|
|
207
207
|
parser.add_argument("--force", action="store_true", help="Force overwrite when adding a model")
|
|
208
|
+
return parser
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main():
|
|
212
|
+
parser = build_parser()
|
|
208
213
|
args = parser.parse_args()
|
|
209
214
|
|
|
210
215
|
cfg = load_config()
|
lemonpie_cli-0.2.0a1/src/lemonpie/cli_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/cli/parser.py
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
|
|
3
|
-
from lemonpie.session_utils import list_sessions, read_current
|
|
4
|
-
|
|
5
3
|
try:
|
|
6
4
|
from lemonpie._version import __version__
|
|
7
5
|
except ImportError:
|
|
8
6
|
__version__ = "0.0.0.dev0"
|
|
9
7
|
|
|
8
|
+
|
|
10
9
|
def build_parser():
|
|
11
10
|
parser = argparse.ArgumentParser(prog="lm", description="Run LemonPie with a given model or prompt")
|
|
12
11
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
@@ -21,22 +20,3 @@ def build_parser():
|
|
|
21
20
|
parser.add_argument("--delete-all", action="store_true", help="Delete all stored sessions")
|
|
22
21
|
parser.add_argument("prompt", nargs="*", help="Prompt text")
|
|
23
22
|
return parser
|
|
24
|
-
|
|
25
|
-
def confirm(prompt):
|
|
26
|
-
ans = input(f"{prompt} (y/N): ").strip().lower()
|
|
27
|
-
return ans in ("y", "yes")
|
|
28
|
-
|
|
29
|
-
def print_session(session):
|
|
30
|
-
print(f"Session {session['id']} — {session.get('title') or '<no title>'}")
|
|
31
|
-
for turn in session["history"]:
|
|
32
|
-
role = turn["role"]
|
|
33
|
-
content = turn["content"]
|
|
34
|
-
print(f"{role} [{turn.get('ts','')}] : {content}")
|
|
35
|
-
|
|
36
|
-
def print_sessions_list():
|
|
37
|
-
sessions = list_sessions()
|
|
38
|
-
if not sessions:
|
|
39
|
-
print("No sessions found.")
|
|
40
|
-
return
|
|
41
|
-
for sid, title, created, updated, is_current in sessions:
|
|
42
|
-
print(f"{is_current} {sid} {title} created: {created}, updated: {updated}")
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import random
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from lemonpie.sessions.storage import list_sessions
|
|
2
7
|
|
|
3
8
|
CONNECTING_MSGS = [
|
|
4
9
|
"Connecting",
|
|
5
10
|
"Squeezing the lemons",
|
|
6
|
-
"Knocking on Ollama’s door"
|
|
11
|
+
"Knocking on Ollama’s door",
|
|
7
12
|
]
|
|
8
13
|
|
|
9
14
|
WAITING_MSGS = [
|
|
10
15
|
"Waiting for model response",
|
|
11
16
|
"The pie is cooking",
|
|
12
|
-
"Brewing tokens"
|
|
17
|
+
"Brewing tokens",
|
|
13
18
|
]
|
|
14
19
|
|
|
20
|
+
|
|
21
|
+
def confirm(prompt):
|
|
22
|
+
ans = input(f"{prompt} (y/N): ").strip().lower()
|
|
23
|
+
return ans in ("y", "yes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_session(session):
|
|
27
|
+
print(f"Session {session['id']} — {session.get('title') or '<no title>'}")
|
|
28
|
+
for turn in session["history"]:
|
|
29
|
+
role = turn["role"]
|
|
30
|
+
content = turn["content"]
|
|
31
|
+
print(f"{role} [{turn.get('ts', '')}] : {content}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_sessions_list():
|
|
35
|
+
sessions = list_sessions()
|
|
36
|
+
if not sessions:
|
|
37
|
+
print("No sessions found.")
|
|
38
|
+
return
|
|
39
|
+
for sid, title, created, updated, is_current in sessions:
|
|
40
|
+
print(f"{is_current} {sid} {title} created: {created}, updated: {updated}")
|
|
41
|
+
|
|
42
|
+
|
|
15
43
|
class StatusSpinner:
|
|
16
44
|
def __init__(self, messages, interval=0.5):
|
|
17
45
|
self.messages = messages
|
|
File without changes
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
|
|
3
|
+
from lemonpie.sessions.storage import save_session
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
def find_by_alias(cfg, alias):
|
|
7
|
+
for idx, m in enumerate(cfg.get("models", [])):
|
|
8
|
+
if m.get("alias") == alias:
|
|
9
|
+
return idx, m
|
|
10
|
+
return None, None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_by_name(cfg, name):
|
|
14
|
+
for idx, m in enumerate(cfg.get("models", [])):
|
|
15
|
+
if m.get("name") == name:
|
|
16
|
+
return idx, m
|
|
17
|
+
return None, None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_model(cfg, args_model, default_model_name):
|
|
21
|
+
if args_model:
|
|
22
|
+
_, alias_entry = find_by_alias(cfg, args_model)
|
|
23
|
+
if alias_entry:
|
|
24
|
+
return alias_entry["name"]
|
|
25
|
+
_, name_entry = find_by_name(cfg, args_model)
|
|
26
|
+
if name_entry:
|
|
27
|
+
return name_entry["name"]
|
|
28
|
+
return args_model
|
|
29
|
+
return default_model_name
|
|
5
30
|
|
|
6
31
|
|
|
7
32
|
def handle_model_switch(cfg, session, requested_model):
|
|
8
33
|
"""
|
|
9
|
-
Handles validating a requested model/alias and updates the active session
|
|
34
|
+
Handles validating a requested model/alias and updates the active session
|
|
10
35
|
if confirmed by the user.
|
|
11
36
|
"""
|
|
12
37
|
if not session or not requested_model:
|
|
13
38
|
return session
|
|
14
39
|
|
|
15
|
-
alias_entry = find_by_alias(cfg, requested_model)
|
|
16
|
-
name_entry = find_by_name(cfg, requested_model)
|
|
40
|
+
_, alias_entry = find_by_alias(cfg, requested_model)
|
|
41
|
+
_, name_entry = find_by_name(cfg, requested_model)
|
|
17
42
|
|
|
18
43
|
if alias_entry:
|
|
19
44
|
new_model = alias_entry["name"]
|
|
@@ -34,29 +59,3 @@ def handle_model_switch(cfg, session, requested_model):
|
|
|
34
59
|
print("Aborted model change.")
|
|
35
60
|
|
|
36
61
|
return session
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def find_by_alias(cfg, alias):
|
|
40
|
-
for idx, m in enumerate(cfg.get("models", [])):
|
|
41
|
-
if m.get("alias") == alias:
|
|
42
|
-
return idx, m
|
|
43
|
-
return None, None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def find_by_name(cfg, name):
|
|
47
|
-
for idx, m in enumerate(cfg.get("models", [])):
|
|
48
|
-
if m.get("name") == name:
|
|
49
|
-
return idx, m
|
|
50
|
-
return None, None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def resolve_model(cfg, args_model, default_model_name):
|
|
54
|
-
if args_model:
|
|
55
|
-
alias_entry = find_by_alias(cfg, args_model)
|
|
56
|
-
if alias_entry:
|
|
57
|
-
return alias_entry["name"]
|
|
58
|
-
name_entry = find_by_name(cfg, args_model)
|
|
59
|
-
if name_entry:
|
|
60
|
-
return name_entry["name"]
|
|
61
|
-
return args_model
|
|
62
|
-
return default_model_name
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Ollama API interaction abstractions: client construction and streaming chat."""
|
|
2
|
+
|
|
3
|
+
from ollama import Client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_client(server_host, timeout):
|
|
7
|
+
return Client(host=server_host, timeout=timeout)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stream_chat(client, model, messages):
|
|
11
|
+
"""
|
|
12
|
+
Stream a chat completion from Ollama, yielding text content chunks.
|
|
13
|
+
|
|
14
|
+
The underlying stream is closed automatically when this generator is
|
|
15
|
+
closed (e.g. via a `for` loop finishing, an exception propagating, or an
|
|
16
|
+
explicit `.close()` call on the generator by the caller).
|
|
17
|
+
"""
|
|
18
|
+
stream = client.chat(model=model, messages=messages, stream=True)
|
|
19
|
+
try:
|
|
20
|
+
for chunk in stream:
|
|
21
|
+
# Standard Ollama client attribute access
|
|
22
|
+
content = getattr(getattr(chunk, "message", None), "content", None)
|
|
23
|
+
if content is None and isinstance(chunk, dict):
|
|
24
|
+
content = chunk.get("message", {}).get("content", "")
|
|
25
|
+
if content:
|
|
26
|
+
yield content
|
|
27
|
+
finally:
|
|
28
|
+
if hasattr(stream, "close"):
|
|
29
|
+
stream.close()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_ollama_error(exc, server_host):
|
|
33
|
+
"""
|
|
34
|
+
Map an exception raised during an Ollama request to a (message, hint)
|
|
35
|
+
pair suitable for printing to the user. `hint` may be None.
|
|
36
|
+
"""
|
|
37
|
+
msg = str(exc)
|
|
38
|
+
if "[Errno 111]" in msg or "Connection refused" in msg:
|
|
39
|
+
return (
|
|
40
|
+
"Ollama server did not respond: Connection refused.",
|
|
41
|
+
f"Hint: Ollama may not be running, or it may not be listening on: {server_host}\n"
|
|
42
|
+
"Check that the server is started with `ollama serve` and bound to the correct interface.",
|
|
43
|
+
)
|
|
44
|
+
if "[Errno 110]" in msg or "timed out" in msg.lower():
|
|
45
|
+
return (
|
|
46
|
+
"Failed to contact Ollama server: Connection timed out.",
|
|
47
|
+
f"Hint: The server at {server_host} is unreachable. Verify IP/port and network.",
|
|
48
|
+
)
|
|
49
|
+
return (f"Failed to contact Ollama server: {msg}", None)
|
|
@@ -17,11 +17,7 @@ Quick usage
|
|
|
17
17
|
See README.md for details and maintainer notes.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
import
|
|
21
|
-
from datetime import datetime, timezone
|
|
22
|
-
from ollama import Client
|
|
23
|
-
|
|
24
|
-
from lemonpie.paths import CURRENT_FILE, SESSIONS_DIR
|
|
20
|
+
import sys
|
|
25
21
|
|
|
26
22
|
from lemonpie.config import (
|
|
27
23
|
load_config,
|
|
@@ -29,55 +25,46 @@ from lemonpie.config import (
|
|
|
29
25
|
build_timeout,
|
|
30
26
|
)
|
|
31
27
|
|
|
32
|
-
from lemonpie.
|
|
33
|
-
|
|
34
|
-
list_sessions,
|
|
28
|
+
from lemonpie.cli.parser import build_parser
|
|
29
|
+
from lemonpie.cli.ui import (
|
|
35
30
|
print_session,
|
|
36
31
|
print_sessions_list,
|
|
32
|
+
StatusSpinner,
|
|
33
|
+
CONNECTING_MSGS,
|
|
34
|
+
WAITING_MSGS,
|
|
37
35
|
)
|
|
38
36
|
|
|
39
|
-
from lemonpie.
|
|
40
|
-
find_by_alias,
|
|
37
|
+
from lemonpie.engine.models import (
|
|
41
38
|
find_by_name,
|
|
42
39
|
handle_model_switch,
|
|
43
40
|
resolve_model,
|
|
44
41
|
)
|
|
45
42
|
|
|
46
|
-
from lemonpie.
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
from lemonpie.engine.ollama import (
|
|
44
|
+
build_client,
|
|
45
|
+
stream_chat,
|
|
46
|
+
resolve_ollama_error,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from lemonpie.sessions.storage import (
|
|
49
50
|
delete_session_by_id,
|
|
50
51
|
delete_all_sessions,
|
|
51
52
|
resolve_session,
|
|
52
53
|
close_session,
|
|
53
54
|
load_session,
|
|
54
55
|
save_session,
|
|
55
|
-
delete_session,
|
|
56
56
|
read_current,
|
|
57
|
-
write_current,
|
|
58
|
-
clear_current,
|
|
59
|
-
default_title,
|
|
60
|
-
set_title,
|
|
61
57
|
update_title,
|
|
62
58
|
)
|
|
63
59
|
|
|
64
|
-
from lemonpie.spinner import (
|
|
65
|
-
StatusSpinner,
|
|
66
|
-
CONNECTING_MSGS,
|
|
67
|
-
WAITING_MSGS,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
60
|
from lemonpie.time_utils import now_ts
|
|
71
61
|
|
|
72
|
-
cfg = load_config()
|
|
73
|
-
server_host = resolve_server_host(cfg)
|
|
74
|
-
timeout = build_timeout()
|
|
75
|
-
client = Client(host=server_host, timeout=timeout)
|
|
76
|
-
|
|
77
|
-
models_list = cfg["models"]
|
|
78
|
-
default_model_name = cfg.get("default") or None
|
|
79
|
-
|
|
80
62
|
def main():
|
|
63
|
+
cfg = load_config()
|
|
64
|
+
server_host = resolve_server_host(cfg)
|
|
65
|
+
timeout = build_timeout()
|
|
66
|
+
client = build_client(server_host, timeout)
|
|
67
|
+
default_model_name = cfg.get("default") or None
|
|
81
68
|
parser = build_parser()
|
|
82
69
|
args = parser.parse_args()
|
|
83
70
|
|
|
@@ -85,18 +72,9 @@ def main():
|
|
|
85
72
|
parser.print_help()
|
|
86
73
|
sys.exit(1)
|
|
87
74
|
|
|
88
|
-
model_flag = any(a in ("-m", "--model") for a in sys.argv[1:])
|
|
89
|
-
resolve_model(cfg, args.model, default_model_name)
|
|
90
|
-
|
|
91
|
-
model = default_model_name
|
|
92
|
-
if model is None:
|
|
93
|
-
print("No default model configured.")
|
|
94
|
-
print("Use lmConfig to add one or pass a known alias with -m.")
|
|
95
|
-
sys.exit(1)
|
|
96
|
-
|
|
97
75
|
current_id = read_current()
|
|
98
76
|
|
|
99
|
-
|
|
77
|
+
# Direct CLI actions
|
|
100
78
|
|
|
101
79
|
if args.list:
|
|
102
80
|
print_sessions_list(); sys.exit(0)
|
|
@@ -122,10 +100,18 @@ def main():
|
|
|
122
100
|
confirm = input("Are you sure you want to delete ALL sessions? (y/N): ")
|
|
123
101
|
if confirm.lower() in ("y", "yes"):
|
|
124
102
|
count = delete_all_sessions()
|
|
125
|
-
print(f"Deleted {count} session(s).")
|
|
103
|
+
print(f"Deleted {count} session(s)."); sys.exit(0)
|
|
126
104
|
else:
|
|
127
105
|
print("Aborted."); sys.exit(0)
|
|
128
106
|
|
|
107
|
+
model_flag = args.model is not None
|
|
108
|
+
model = resolve_model(cfg, args.model, default_model_name)
|
|
109
|
+
|
|
110
|
+
if model is None:
|
|
111
|
+
print("No default model configured.")
|
|
112
|
+
print("Use lmConfig to add one or pass a known alias with -m.")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
129
115
|
if model_flag and not current_id and not args.new and not args.session and not args.prompt:
|
|
130
116
|
print(f'No current session.\n'
|
|
131
117
|
f'To create a new session with "{args.model}" use: lm -n -m {args.model}\n'
|
|
@@ -141,17 +127,15 @@ def main():
|
|
|
141
127
|
|
|
142
128
|
if args.title:
|
|
143
129
|
session = update_title(session, model, args.title, args.prompt)
|
|
130
|
+
if not args.prompt:
|
|
131
|
+
sys.exit(0)
|
|
144
132
|
|
|
145
133
|
if args.prompt:
|
|
146
134
|
user_prompt = " ".join(args.prompt)
|
|
147
135
|
turn_ts = now_ts()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
save_session(session_id, session)
|
|
152
|
-
else:
|
|
153
|
-
session = create_session(user_prompt, model)
|
|
154
|
-
session_id = session["id"]
|
|
136
|
+
session["history"].append({"role": "user", "content": user_prompt, "ts": turn_ts})
|
|
137
|
+
session["updated_at"] = turn_ts
|
|
138
|
+
save_session(session_id, session)
|
|
155
139
|
|
|
156
140
|
if not find_by_name(cfg, model):
|
|
157
141
|
print(f'Configured models do not include "{model}". Use lmConfig to add it or pass a known alias.')
|
|
@@ -163,50 +147,28 @@ def main():
|
|
|
163
147
|
assistant_reply = ""
|
|
164
148
|
reply_ts = None
|
|
165
149
|
first_chunk_seen = False
|
|
150
|
+
chunks = stream_chat(client, model, session["history"])
|
|
166
151
|
|
|
167
152
|
try:
|
|
168
|
-
|
|
169
|
-
spinner.stop()
|
|
170
|
-
|
|
171
|
-
spinner = StatusSpinner(WAITING_MSGS)
|
|
172
|
-
spinner.start()
|
|
173
|
-
|
|
174
|
-
for chunk in stream:
|
|
153
|
+
for content in chunks:
|
|
175
154
|
if not first_chunk_seen:
|
|
176
155
|
spinner.stop()
|
|
177
156
|
first_chunk_seen = True
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
content = getattr(getattr(chunk, "message", None), "content", None)
|
|
181
|
-
if content is None and isinstance(chunk, dict):
|
|
182
|
-
content = chunk.get("message", {}).get("content", "")
|
|
183
|
-
|
|
184
|
-
if content:
|
|
185
|
-
print(content, end="", flush=True)
|
|
186
|
-
assistant_reply += content
|
|
157
|
+
print(content, end="", flush=True)
|
|
158
|
+
assistant_reply += content
|
|
187
159
|
print()
|
|
188
160
|
reply_ts = now_ts()
|
|
189
161
|
|
|
190
162
|
except KeyboardInterrupt:
|
|
191
|
-
|
|
192
|
-
if 'stream' in locals() and hasattr(stream, "close"):
|
|
193
|
-
stream.close()
|
|
194
|
-
except Exception:
|
|
195
|
-
pass
|
|
163
|
+
chunks.close()
|
|
196
164
|
print("\nInterrupted by user.")
|
|
197
165
|
sys.exit(1)
|
|
198
166
|
|
|
199
167
|
except Exception as e:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
print(
|
|
204
|
-
print("Check that the server is started with `ollama serve` and bound to the correct interface.")
|
|
205
|
-
elif "[Errno 110]" in msg or "timed out" in msg.lower():
|
|
206
|
-
print("\nFailed to contact Ollama server: Connection timed out.")
|
|
207
|
-
print("Hint: The server at", server_host, "is unreachable. Verify IP/port and network.")
|
|
208
|
-
else:
|
|
209
|
-
print("\nFailed to contact Ollama server:", msg)
|
|
168
|
+
title, hint = resolve_ollama_error(e, server_host)
|
|
169
|
+
print("\n" + title)
|
|
170
|
+
if hint:
|
|
171
|
+
print(hint)
|
|
210
172
|
sys.exit(1)
|
|
211
173
|
|
|
212
174
|
finally:
|
|
File without changes
|