Files
ragflow/test/test_cajal_template_unit.py
sxxtony 06b07bbfd6 Add CAJAL scientific paper agent template (#14641)
### What problem does this PR solve?

Closes https://github.com/infiniflow/ragflow/issues/14571.

Adds CAJAL as a first-class local scientific-writing option in RAGFlow:

- registers `agnuxo/cajal-4b-p2pclaw` as a known Ollama chat model with
a 32K context setting
- adds a built-in “CAJAL scientific paper agent” template under the
existing agent template catalog
- preconfigures the agent for grounded scientific writing: retrieval
first, citation traceability, LaTeX-ready output, and explicit
limitations when evidence is missing
- adds unit coverage to ensure the template normalizes through RAGFlow’s
production template loader, keeps graph form data in sync, and exposes
the Ollama model option

Behavior/evidence gathered for the requested model:

- Hugging Face model metadata for `Agnuxo/CAJAL-4B-P2PCLAW` reports
`pipeline_tag=text-generation` and tags including `gguf`, `llama.cpp`,
`vllm`, `scientific-research`, `papers`, `academic-writing`, `latex`,
and `license:apache-2.0`.
- The model card documents CAJAL as a 4B scientific paper generation
model with 32K context, local inference, LaTeX/citation specialization,
and CPU-only support around 5 tok/s on Ryzen 7 5800X.
- Local CPU generation could not be completed on this machine because
the advertised Ollama model name is not currently resolvable from
Ollama’s registry: both
`https://registry.ollama.ai/v2/agnuxo/cajal-4b-p2pclaw/manifests/latest`
and
`https://registry.ollama.ai/v2/library/agnuxo/cajal-4b-p2pclaw/manifests/latest`
returned `404 Not Found`; the Hugging Face repo tree currently exposes
an 8.4 GB `model.safetensors` but no GGUF artifact in `main`. The
template therefore targets the documented Ollama model name for users
who have the local CAJAL deployment/model file available.

Verification run locally:

```bash
python3 -m pytest test/test_cajal_template_unit.py -q
# 3 passed in 0.34s

python3 - <<'PY'
import json, glob
for f in sorted(glob.glob('agent/templates/*.json') + ['conf/llm_factories.json']):
    with open(f, encoding='utf-8') as fp: json.load(fp)
print('json_ok')
PY
# json_ok

python3 -m ruff check test/test_cajal_template_unit.py
# All checks passed!

git diff --check
```

`uv run pytest
test/testcases/test_web_api/test_agent_app/test_cajal_template_unit.py
-q` was also attempted first, but dependency setup failed before test
collection while building `ormsgpack==1.5.0` from uv with a package
metadata parse error. Clearing uv’s `ormsgpack` cache and retrying
reproduced the same build failure, so the focused unit test was run with
the system Python environment instead.

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

---------

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
Co-authored-by: yzc <yzc@users.noreply.github.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-01 09:35:37 +08:00

80 lines
3.5 KiB
Python

#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import importlib.util
from pathlib import Path
def _load_template_utils():
repo_root = Path(__file__).resolve().parents[1]
module_path = repo_root / "api" / "db" / "template_utils.py"
spec = importlib.util.spec_from_file_location("template_utils", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _load_cajal_template():
repo_root = Path(__file__).resolve().parents[1]
template_path = repo_root / "agent" / "templates" / "cajal_scientific_paper_agent.json"
with template_path.open(encoding="utf-8") as template_file:
return _load_template_utils().normalize_canvas_template_categories(json.load(template_file))
def test_cajal_template_exposes_local_ollama_model_and_agent_categories():
template = _load_cajal_template()
assert template["id"] == "41"
assert template["title"]["en"] == "CAJAL scientific paper agent"
assert template["canvas_type"] == "Agent"
assert template["canvas_types"] == ["Agent", "Recommended"]
agent_params = template["dsl"]["components"]["Agent:NewPumasLick"]["obj"]["params"]
assert agent_params["llm_id"] == "agnuxo/cajal-4b-p2pclaw@Ollama"
assert agent_params["max_tokens"] == 32768
assert "Agnuxo/CAJAL-4B-P2PCLAW" in agent_params["sys_prompt"]
assert "LaTeX" in agent_params["sys_prompt"]
def test_cajal_template_keeps_retrieval_grounding_and_graph_form_in_sync():
template = _load_cajal_template()
agent_params = template["dsl"]["components"]["Agent:NewPumasLick"]["obj"]["params"]
retrieval_tools = [tool for tool in agent_params["tools"] if tool["component_name"] == "Retrieval"]
assert len(retrieval_tools) == 1
assert retrieval_tools[0]["params"]["top_n"] == 10
assert "ground" in retrieval_tools[0]["params"]["description"].lower()
assert "{sys.query}" in agent_params["prompts"][0]["content"]
agent_node = next(node for node in template["dsl"]["graph"]["nodes"] if node["id"] == "Agent:NewPumasLick")
begin_node = next(node for node in template["dsl"]["graph"]["nodes"] if node["id"] == "begin")
assert agent_node["data"]["form"]["llm_id"] == agent_params["llm_id"]
assert agent_node["data"]["form"]["sys_prompt"] == agent_params["sys_prompt"]
assert "CAJAL" in begin_node["data"]["form"]["prologue"]
def test_cajal_is_registered_as_a_known_ollama_chat_model():
repo_root = Path(__file__).resolve().parents[1]
factories_path = repo_root / "conf" / "llm_factories.json"
factories = json.loads(factories_path.read_text(encoding="utf-8"))
ollama = next(factory for factory in factories["factory_llm_infos"] if factory["name"] == "Ollama")
cajal = next(model for model in ollama["llm"] if model["llm_name"] == "agnuxo/cajal-4b-p2pclaw")
assert cajal["model_type"] == "chat"
assert cajal["max_tokens"] == 32768
assert "SCIENTIFIC_WRITING" in cajal["tags"]