Documented use of subscription GenAI or Ollama as an easier LLM path.
This commit is contained in:
parent
0f905b5a22
commit
aac1e5c8bc
18
README.md
18
README.md
|
|
@ -198,6 +198,23 @@ python -m didactopus.model_bench
|
||||||
|
|
||||||
It evaluates local-model adequacy for the `mentor`, `practice`, and `evaluator` roles using the MIT OCW skill bundle as grounded context.
|
It evaluates local-model adequacy for the `mentor`, `practice`, and `evaluator` roles using the MIT OCW skill bundle as grounded context.
|
||||||
|
|
||||||
|
### Easiest LLM setup paths
|
||||||
|
|
||||||
|
If you want live LLM-backed Didactopus behavior without the complexity of RoleMesh, start with one of these:
|
||||||
|
|
||||||
|
1. `ollama` for simple local use
|
||||||
|
2. `openai_compatible` for simple hosted use
|
||||||
|
3. `rolemesh` only if you need routing and multi-model orchestration
|
||||||
|
|
||||||
|
The two low-friction starting configs are:
|
||||||
|
|
||||||
|
- `configs/config.ollama.example.yaml`
|
||||||
|
- `configs/config.openai-compatible.example.yaml`
|
||||||
|
|
||||||
|
For setup details, see:
|
||||||
|
|
||||||
|
- `docs/model-provider-setup.md`
|
||||||
|
|
||||||
## What Is In This Repository
|
## What Is In This Repository
|
||||||
|
|
||||||
- `src/didactopus/`
|
- `src/didactopus/`
|
||||||
|
|
@ -451,6 +468,7 @@ What remains heuristic or lightweight:
|
||||||
- [docs/roadmap.md](docs/roadmap.md)
|
- [docs/roadmap.md](docs/roadmap.md)
|
||||||
- [docs/learner-accessibility.md](docs/learner-accessibility.md)
|
- [docs/learner-accessibility.md](docs/learner-accessibility.md)
|
||||||
- [docs/local-model-benchmark.md](docs/local-model-benchmark.md)
|
- [docs/local-model-benchmark.md](docs/local-model-benchmark.md)
|
||||||
|
- [docs/model-provider-setup.md](docs/model-provider-setup.md)
|
||||||
- [docs/course-to-pack.md](docs/course-to-pack.md)
|
- [docs/course-to-pack.md](docs/course-to-pack.md)
|
||||||
- [docs/learning-graph.md](docs/learning-graph.md)
|
- [docs/learning-graph.md](docs/learning-graph.md)
|
||||||
- [docs/agentic-learner-loop.md](docs/agentic-learner-loop.md)
|
- [docs/agentic-learner-loop.md](docs/agentic-learner-loop.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
review:
|
||||||
|
default_reviewer: "Wesley R. Elsberry"
|
||||||
|
write_promoted_pack: true
|
||||||
|
|
||||||
|
bridge:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 8765
|
||||||
|
registry_path: "workspace_registry.json"
|
||||||
|
default_workspace_root: "workspaces"
|
||||||
|
|
||||||
|
model_provider:
|
||||||
|
provider: "ollama"
|
||||||
|
ollama:
|
||||||
|
base_url: "http://127.0.0.1:11434/v1"
|
||||||
|
api_key: "ollama"
|
||||||
|
# Set this to a model you have already pulled with `ollama pull ...`.
|
||||||
|
default_model: "llama3.2:3b"
|
||||||
|
role_to_model:
|
||||||
|
mentor: "llama3.2:3b"
|
||||||
|
learner: "llama3.2:3b"
|
||||||
|
practice: "llama3.2:3b"
|
||||||
|
project_advisor: "llama3.2:3b"
|
||||||
|
evaluator: "llama3.2:3b"
|
||||||
|
timeout_seconds: 90.0
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
review:
|
||||||
|
default_reviewer: "Wesley R. Elsberry"
|
||||||
|
write_promoted_pack: true
|
||||||
|
|
||||||
|
bridge:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 8765
|
||||||
|
registry_path: "workspace_registry.json"
|
||||||
|
default_workspace_root: "workspaces"
|
||||||
|
|
||||||
|
model_provider:
|
||||||
|
provider: "openai_compatible"
|
||||||
|
openai_compatible:
|
||||||
|
# For OpenAI itself, leave this as https://api.openai.com/v1
|
||||||
|
# For another OpenAI-compatible hosted service, change the base URL and model names.
|
||||||
|
base_url: "https://api.openai.com/v1"
|
||||||
|
api_key: "set-me-via-env-or-local-config"
|
||||||
|
default_model: "gpt-4.1-mini"
|
||||||
|
role_to_model:
|
||||||
|
mentor: "gpt-4.1-mini"
|
||||||
|
learner: "gpt-4.1-mini"
|
||||||
|
practice: "gpt-4.1-mini"
|
||||||
|
project_advisor: "gpt-4.1-mini"
|
||||||
|
evaluator: "gpt-4.1-mini"
|
||||||
|
timeout_seconds: 60.0
|
||||||
|
auth_scheme: "bearer"
|
||||||
25
docs/faq.md
25
docs/faq.md
|
|
@ -107,6 +107,31 @@ There are now two learner paths in the repo.
|
||||||
|
|
||||||
So the deterministic learner is still active, but it is no longer the only learner-style path shown in the repository.
|
So the deterministic learner is still active, but it is no longer the only learner-style path shown in the repository.
|
||||||
|
|
||||||
|
## What is the easiest way to use a live LLM with Didactopus?
|
||||||
|
|
||||||
|
Start with either:
|
||||||
|
|
||||||
|
- `configs/config.ollama.example.yaml` for simple local use
|
||||||
|
- `configs/config.openai-compatible.example.yaml` for simple hosted use
|
||||||
|
|
||||||
|
RoleMesh is still supported, but it is now the advanced option for users who actually need routing and multiple backends.
|
||||||
|
|
||||||
|
The simplest local command shape is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m didactopus.learner_session_demo --config configs/config.ollama.example.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The simplest hosted command shape is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m didactopus.learner_session_demo --config configs/config.openai-compatible.example.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
For the full setup notes, see:
|
||||||
|
|
||||||
|
- `docs/model-provider-setup.md`
|
||||||
|
|
||||||
## Can I still use it as a personal mentor even though the learner is synthetic?
|
## Can I still use it as a personal mentor even though the learner is synthetic?
|
||||||
|
|
||||||
Yes, if you think of the current repo as a structured learning workbench rather than a chat product.
|
Yes, if you think of the current repo as a structured learning workbench rather than a chat product.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
# Model Provider Setup
|
||||||
|
|
||||||
|
Didactopus now supports three main model-provider paths:
|
||||||
|
|
||||||
|
- `ollama`
|
||||||
|
- easiest local setup for most single users
|
||||||
|
- `openai_compatible`
|
||||||
|
- simplest hosted setup when you want a common online API
|
||||||
|
- `rolemesh`
|
||||||
|
- more flexible routing for technically oriented users, labs, and libraries
|
||||||
|
|
||||||
|
## Recommended Order
|
||||||
|
|
||||||
|
For ease of adoption, use these in this order:
|
||||||
|
|
||||||
|
1. `ollama`
|
||||||
|
2. `openai_compatible`
|
||||||
|
3. `rolemesh`
|
||||||
|
|
||||||
|
## Option 1: Ollama
|
||||||
|
|
||||||
|
This is the easiest local path for most users.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- `configs/config.ollama.example.yaml`
|
||||||
|
|
||||||
|
Minimal setup:
|
||||||
|
|
||||||
|
1. Install Ollama.
|
||||||
|
2. Pull a model you want to use.
|
||||||
|
3. Start or verify the local Ollama service.
|
||||||
|
4. Point Didactopus at `configs/config.ollama.example.yaml`.
|
||||||
|
|
||||||
|
Example commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama pull llama3.2:3b
|
||||||
|
python -m didactopus.learner_session_demo --config configs/config.ollama.example.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want a different local model, change:
|
||||||
|
|
||||||
|
- `model_provider.ollama.default_model`
|
||||||
|
- `model_provider.ollama.role_to_model`
|
||||||
|
|
||||||
|
Use one model for every role at first. Split roles only if you have a reason to do so.
|
||||||
|
|
||||||
|
## Option 2: OpenAI-compatible hosted service
|
||||||
|
|
||||||
|
This is the easiest hosted path.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- `configs/config.openai-compatible.example.yaml`
|
||||||
|
|
||||||
|
This works for:
|
||||||
|
|
||||||
|
- OpenAI itself
|
||||||
|
- any hosted service that accepts OpenAI-style `POST /v1/chat/completions`
|
||||||
|
|
||||||
|
Typical setup:
|
||||||
|
|
||||||
|
1. Create a local copy of `configs/config.openai-compatible.example.yaml`.
|
||||||
|
2. Set `base_url`, `api_key`, and `default_model`.
|
||||||
|
3. Keep one model for all roles to start with.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m didactopus.learner_session_demo --config configs/config.openai-compatible.example.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option 3: RoleMesh Gateway
|
||||||
|
|
||||||
|
RoleMesh is still useful, but it is no longer the easiest path to recommend to most users.
|
||||||
|
|
||||||
|
Choose it when you need:
|
||||||
|
|
||||||
|
- role-specific routing
|
||||||
|
- multiple local or remote backends
|
||||||
|
- heterogeneous compute placement
|
||||||
|
- a shared service for a library, lab, or multi-user setup
|
||||||
|
|
||||||
|
See:
|
||||||
|
|
||||||
|
- `docs/rolemesh-integration.md`
|
||||||
|
|
||||||
|
## Which commands use the provider?
|
||||||
|
|
||||||
|
Any Didactopus path that calls the model provider can use these configurations, including:
|
||||||
|
|
||||||
|
- `python -m didactopus.learner_session_demo`
|
||||||
|
- `python -m didactopus.rolemesh_demo`
|
||||||
|
- `python -m didactopus.model_bench`
|
||||||
|
- `python -m didactopus.ocw_rolemesh_transcript_demo`
|
||||||
|
|
||||||
|
The transcript demo name still references RoleMesh because that was the original live-LLM path, but the general learner-session and benchmark flows are the easier places to start.
|
||||||
|
|
||||||
|
## Practical Advice
|
||||||
|
|
||||||
|
- Start with one model for all roles.
|
||||||
|
- Prefer smaller fast models over bigger slow ones at first.
|
||||||
|
- Use the benchmark harness before trusting a model for learner-facing guidance.
|
||||||
|
- Use RoleMesh only when you actually need routing or multi-model orchestration.
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
# RoleMesh Integration
|
# RoleMesh Integration
|
||||||
|
|
||||||
RoleMesh Gateway is an appropriate dependency for local-LLM-backed Didactopus usage.
|
RoleMesh Gateway is an appropriate dependency for local-LLM-backed Didactopus usage, but it should be treated as the advanced path rather than the default path for most users.
|
||||||
|
|
||||||
|
If ease of use is your priority, start with:
|
||||||
|
|
||||||
|
- `docs/model-provider-setup.md`
|
||||||
|
- `configs/config.ollama.example.yaml`
|
||||||
|
- `configs/config.openai-compatible.example.yaml`
|
||||||
|
|
||||||
|
Use RoleMesh when you need routing flexibility, multiple backends, or shared infrastructure.
|
||||||
|
|
||||||
## Why it fits
|
## Why it fits
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,29 @@ class RoleMeshProviderConfig(BaseModel):
|
||||||
timeout_seconds: float = 30.0
|
timeout_seconds: float = 30.0
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaProviderConfig(BaseModel):
|
||||||
|
base_url: str = os.getenv("DIDACTOPUS_OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1")
|
||||||
|
api_key: str = os.getenv("DIDACTOPUS_OLLAMA_API_KEY", "ollama")
|
||||||
|
default_model: str = os.getenv("DIDACTOPUS_OLLAMA_DEFAULT_MODEL", "llama3.2:3b")
|
||||||
|
role_to_model: dict[str, str] = Field(default_factory=default_role_to_model)
|
||||||
|
timeout_seconds: float = 60.0
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAICompatibleProviderConfig(BaseModel):
|
||||||
|
base_url: str = os.getenv("DIDACTOPUS_OPENAI_COMPAT_BASE_URL", "https://api.openai.com/v1")
|
||||||
|
api_key: str = os.getenv("DIDACTOPUS_OPENAI_COMPAT_API_KEY", "")
|
||||||
|
default_model: str = os.getenv("DIDACTOPUS_OPENAI_COMPAT_DEFAULT_MODEL", "gpt-4.1-mini")
|
||||||
|
role_to_model: dict[str, str] = Field(default_factory=default_role_to_model)
|
||||||
|
timeout_seconds: float = 60.0
|
||||||
|
auth_scheme: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
class ModelProviderConfig(BaseModel):
|
class ModelProviderConfig(BaseModel):
|
||||||
provider: str = "stub"
|
provider: str = "stub"
|
||||||
local: LocalProviderConfig = Field(default_factory=LocalProviderConfig)
|
local: LocalProviderConfig = Field(default_factory=LocalProviderConfig)
|
||||||
rolemesh: RoleMeshProviderConfig = Field(default_factory=RoleMeshProviderConfig)
|
rolemesh: RoleMeshProviderConfig = Field(default_factory=RoleMeshProviderConfig)
|
||||||
|
ollama: OllamaProviderConfig = Field(default_factory=OllamaProviderConfig)
|
||||||
|
openai_compatible: OpenAICompatibleProviderConfig = Field(default_factory=OpenAICompatibleProviderConfig)
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ class ModelProvider:
|
||||||
provider_name = self.config.provider.lower()
|
provider_name = self.config.provider.lower()
|
||||||
if provider_name == "rolemesh":
|
if provider_name == "rolemesh":
|
||||||
return self._generate_rolemesh(prompt, role, system_prompt, temperature, max_tokens, status_callback)
|
return self._generate_rolemesh(prompt, role, system_prompt, temperature, max_tokens, status_callback)
|
||||||
|
if provider_name == "ollama":
|
||||||
|
return self._generate_ollama(prompt, role, system_prompt, temperature, max_tokens, status_callback)
|
||||||
|
if provider_name == "openai_compatible":
|
||||||
|
return self._generate_openai_compatible(prompt, role, system_prompt, temperature, max_tokens, status_callback)
|
||||||
return self._generate_stub(prompt, role)
|
return self._generate_stub(prompt, role)
|
||||||
|
|
||||||
def _generate_stub(self, prompt: str, role: str | None) -> ModelResponse:
|
def _generate_stub(self, prompt: str, role: str | None) -> ModelResponse:
|
||||||
|
|
@ -77,28 +81,119 @@ class ModelProvider:
|
||||||
if max_tokens is not None:
|
if max_tokens is not None:
|
||||||
payload["max_tokens"] = max_tokens
|
payload["max_tokens"] = max_tokens
|
||||||
body = self._rolemesh_chat_completion(payload)
|
body = self._rolemesh_chat_completion(payload)
|
||||||
|
return self._response_from_chat_completion(body, provider="rolemesh", model_name=model_name)
|
||||||
|
|
||||||
|
def _generate_ollama(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
role: str | None,
|
||||||
|
system_prompt: str | None,
|
||||||
|
temperature: float | None,
|
||||||
|
max_tokens: int | None,
|
||||||
|
status_callback: Callable[[str], None] | None,
|
||||||
|
) -> ModelResponse:
|
||||||
|
ollama = self.config.ollama
|
||||||
|
model_name = ollama.role_to_model.get(role or "", ollama.default_model)
|
||||||
|
if status_callback is not None:
|
||||||
|
status_callback(self.pending_notice(role, model_name))
|
||||||
|
payload = self._build_messages_payload(prompt, system_prompt, model_name, temperature, max_tokens)
|
||||||
|
body = self._chat_completion_request(
|
||||||
|
base_url=ollama.base_url,
|
||||||
|
api_key=ollama.api_key,
|
||||||
|
timeout_seconds=ollama.timeout_seconds,
|
||||||
|
payload=payload,
|
||||||
|
auth_scheme="bearer",
|
||||||
|
)
|
||||||
|
return self._response_from_chat_completion(body, provider="ollama", model_name=model_name)
|
||||||
|
|
||||||
|
def _generate_openai_compatible(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
role: str | None,
|
||||||
|
system_prompt: str | None,
|
||||||
|
temperature: float | None,
|
||||||
|
max_tokens: int | None,
|
||||||
|
status_callback: Callable[[str], None] | None,
|
||||||
|
) -> ModelResponse:
|
||||||
|
compat = self.config.openai_compatible
|
||||||
|
model_name = compat.role_to_model.get(role or "", compat.default_model)
|
||||||
|
if status_callback is not None:
|
||||||
|
status_callback(self.pending_notice(role, model_name))
|
||||||
|
payload = self._build_messages_payload(prompt, system_prompt, model_name, temperature, max_tokens)
|
||||||
|
body = self._chat_completion_request(
|
||||||
|
base_url=compat.base_url,
|
||||||
|
api_key=compat.api_key,
|
||||||
|
timeout_seconds=compat.timeout_seconds,
|
||||||
|
payload=payload,
|
||||||
|
auth_scheme=compat.auth_scheme,
|
||||||
|
)
|
||||||
|
return self._response_from_chat_completion(body, provider="openai_compatible", model_name=model_name)
|
||||||
|
|
||||||
|
def _build_messages_payload(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
system_prompt: str | None,
|
||||||
|
model_name: str,
|
||||||
|
temperature: float | None,
|
||||||
|
max_tokens: int | None,
|
||||||
|
) -> dict:
|
||||||
|
messages = []
|
||||||
|
if system_prompt:
|
||||||
|
messages.append({"role": "system", "content": system_prompt})
|
||||||
|
messages.append({"role": "user", "content": prompt})
|
||||||
|
payload = {
|
||||||
|
"model": model_name,
|
||||||
|
"messages": messages,
|
||||||
|
}
|
||||||
|
if temperature is not None:
|
||||||
|
payload["temperature"] = temperature
|
||||||
|
if max_tokens is not None:
|
||||||
|
payload["max_tokens"] = max_tokens
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _response_from_chat_completion(self, body: dict, *, provider: str, model_name: str) -> ModelResponse:
|
||||||
choices = body.get("choices", [])
|
choices = body.get("choices", [])
|
||||||
if not choices:
|
if not choices:
|
||||||
raise RuntimeError("RoleMesh returned no choices.")
|
raise RuntimeError(f"{provider} returned no choices.")
|
||||||
message = choices[0].get("message", {})
|
message = choices[0].get("message", {})
|
||||||
text = message.get("content", "")
|
text = message.get("content", "")
|
||||||
if not isinstance(text, str):
|
if not isinstance(text, str):
|
||||||
text = str(text)
|
text = str(text)
|
||||||
return ModelResponse(text=text, provider="rolemesh", model_name=model_name)
|
return ModelResponse(text=text, provider=provider, model_name=model_name)
|
||||||
|
|
||||||
def _rolemesh_chat_completion(self, payload: dict) -> dict:
|
def _rolemesh_chat_completion(self, payload: dict) -> dict:
|
||||||
rolemesh = self.config.rolemesh
|
rolemesh = self.config.rolemesh
|
||||||
url = rolemesh.base_url.rstrip("/") + "/v1/chat/completions"
|
return self._chat_completion_request(
|
||||||
|
base_url=rolemesh.base_url,
|
||||||
|
api_key=rolemesh.api_key,
|
||||||
|
timeout_seconds=rolemesh.timeout_seconds,
|
||||||
|
payload=payload,
|
||||||
|
auth_scheme="x-api-key",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _chat_completion_request(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
base_url: str,
|
||||||
|
api_key: str,
|
||||||
|
timeout_seconds: float,
|
||||||
|
payload: dict,
|
||||||
|
auth_scheme: str,
|
||||||
|
) -> dict:
|
||||||
|
url = base_url.rstrip("/") + "/chat/completions" if base_url.rstrip("/").endswith("/v1") else base_url.rstrip("/") + "/v1/chat/completions"
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
if rolemesh.api_key:
|
if api_key:
|
||||||
headers["X-Api-Key"] = rolemesh.api_key
|
if auth_scheme == "x-api-key":
|
||||||
|
headers["X-Api-Key"] = api_key
|
||||||
|
else:
|
||||||
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
req = request.Request(
|
req = request.Request(
|
||||||
url,
|
url,
|
||||||
data=json.dumps(payload).encode("utf-8"),
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
method="POST",
|
method="POST",
|
||||||
)
|
)
|
||||||
with request.urlopen(req, timeout=rolemesh.timeout_seconds) as response:
|
with request.urlopen(req, timeout=timeout_seconds) as response:
|
||||||
return json.loads(response.read().decode("utf-8"))
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,17 @@ def test_load_rolemesh_config() -> None:
|
||||||
assert config.model_provider.rolemesh.role_to_model["mentor"] == "planner"
|
assert config.model_provider.rolemesh.role_to_model["mentor"] == "planner"
|
||||||
assert config.model_provider.rolemesh.role_to_model["learner"] == "writer"
|
assert config.model_provider.rolemesh.role_to_model["learner"] == "writer"
|
||||||
assert set(config.model_provider.rolemesh.role_to_model) == set(role_ids())
|
assert set(config.model_provider.rolemesh.role_to_model) == set(role_ids())
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_ollama_config() -> None:
|
||||||
|
config = load_config(Path("configs/config.ollama.example.yaml"))
|
||||||
|
assert config.model_provider.provider == "ollama"
|
||||||
|
assert config.model_provider.ollama.base_url.endswith("/v1")
|
||||||
|
assert set(config.model_provider.ollama.role_to_model) == set(role_ids())
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_openai_compatible_config() -> None:
|
||||||
|
config = load_config(Path("configs/config.openai-compatible.example.yaml"))
|
||||||
|
assert config.model_provider.provider == "openai_compatible"
|
||||||
|
assert config.model_provider.openai_compatible.base_url == "https://api.openai.com/v1"
|
||||||
|
assert set(config.model_provider.openai_compatible.role_to_model) == set(role_ids())
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,71 @@ def test_rolemesh_provider_emits_pending_notice() -> None:
|
||||||
assert seen == ["Didactopus is evaluating the work before replying. Model: reviewer."]
|
assert seen == ["Didactopus is evaluating the work before replying. Model: reviewer."]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ollama_provider_uses_role_mapping() -> None:
|
||||||
|
config = ModelProviderConfig.model_validate(
|
||||||
|
{
|
||||||
|
"provider": "ollama",
|
||||||
|
"ollama": {
|
||||||
|
"base_url": "http://127.0.0.1:11434/v1",
|
||||||
|
"api_key": "ollama",
|
||||||
|
"default_model": "llama3.2:3b",
|
||||||
|
"role_to_model": {"mentor": "llama3.2:3b", "practice": "qwen2.5:3b"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = ModelProvider(config)
|
||||||
|
|
||||||
|
def fake_chat(*, base_url: str, api_key: str, timeout_seconds: float, payload: dict, auth_scheme: str) -> dict:
|
||||||
|
assert base_url == "http://127.0.0.1:11434/v1"
|
||||||
|
assert api_key == "ollama"
|
||||||
|
assert payload["model"] == "qwen2.5:3b"
|
||||||
|
assert auth_scheme == "bearer"
|
||||||
|
return {"choices": [{"message": {"content": "Ollama practice response"}}]}
|
||||||
|
|
||||||
|
provider._chat_completion_request = fake_chat # type: ignore[method-assign]
|
||||||
|
response = provider.generate(
|
||||||
|
"Generate a practice task.",
|
||||||
|
role="practice",
|
||||||
|
system_prompt="System prompt",
|
||||||
|
)
|
||||||
|
assert response.provider == "ollama"
|
||||||
|
assert response.model_name == "qwen2.5:3b"
|
||||||
|
assert response.text == "Ollama practice response"
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_compatible_provider_uses_bearer_auth() -> None:
|
||||||
|
config = ModelProviderConfig.model_validate(
|
||||||
|
{
|
||||||
|
"provider": "openai_compatible",
|
||||||
|
"openai_compatible": {
|
||||||
|
"base_url": "https://api.openai.com/v1",
|
||||||
|
"api_key": "demo-key",
|
||||||
|
"default_model": "gpt-4.1-mini",
|
||||||
|
"role_to_model": {"mentor": "gpt-4.1-mini"},
|
||||||
|
"auth_scheme": "bearer",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = ModelProvider(config)
|
||||||
|
|
||||||
|
def fake_chat(*, base_url: str, api_key: str, timeout_seconds: float, payload: dict, auth_scheme: str) -> dict:
|
||||||
|
assert base_url == "https://api.openai.com/v1"
|
||||||
|
assert api_key == "demo-key"
|
||||||
|
assert payload["model"] == "gpt-4.1-mini"
|
||||||
|
assert auth_scheme == "bearer"
|
||||||
|
return {"choices": [{"message": {"content": "Hosted mentor response"}}]}
|
||||||
|
|
||||||
|
provider._chat_completion_request = fake_chat # type: ignore[method-assign]
|
||||||
|
response = provider.generate(
|
||||||
|
"Orient the learner.",
|
||||||
|
role="mentor",
|
||||||
|
system_prompt="System prompt",
|
||||||
|
)
|
||||||
|
assert response.provider == "openai_compatible"
|
||||||
|
assert response.model_name == "gpt-4.1-mini"
|
||||||
|
assert response.text == "Hosted mentor response"
|
||||||
|
|
||||||
|
|
||||||
def test_evaluator_prompt_requires_checking_existing_caveats() -> None:
|
def test_evaluator_prompt_requires_checking_existing_caveats() -> None:
|
||||||
prompt = evaluator_system_prompt().lower()
|
prompt = evaluator_system_prompt().lower()
|
||||||
assert "before saying something is missing" in prompt
|
assert "before saying something is missing" in prompt
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue