diff --git a/.gitignore b/.gitignore index 505a3b1..ad2686b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,24 @@ wheels/ # Virtual environments .venv + +# Skills +.codebuddy/ +.agent/ +.agents/ +.claude/ +.cursor/ +.gemini/ +.github/skills/ +.github/prompts/ +.opencode/ +.qoder/ +.qwen/ +.trae/ + +# Environment variables +.env +.env.* +*.env +!.example.env +!*.env.example diff --git a/pyproject.toml b/pyproject.toml index 85aafd1..6f58f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,14 +3,14 @@ name = "llama-agent-skills" version = "0.1.0" description = "Add your description here" readme = "README.md" -authors = [ - { name = "Summer Shen", email = "3244742300@qq.com" } -] +authors = [] requires-python = ">=3.11" dependencies = [ "llama-index-core>=0.12.0", - "llama-index-llms-openai>=0.3.0", + "llama-index-llms-openai>=0.7.5", "python-frontmatter>=1.1.0", + "python-dotenv>=1.0.0", + "llama-index-llms-openai-like>=0.7.1", ] [project.scripts] diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..57a682a --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "excalidraw-diagram-generator": { + "source": "github/awesome-copilot", + "sourceType": "github", + "computedHash": "e24e3c4cab4b5a07bc1d1801db14868f3a1cd184f7f28f5dd2c4a2b234bf2f2d" + } + } +} diff --git a/src/llama_agent_skills/__init__.py b/src/llama_agent_skills/__init__.py index 2078416..c9d3929 100644 --- a/src/llama_agent_skills/__init__.py +++ b/src/llama_agent_skills/__init__.py @@ -5,6 +5,8 @@ import asyncio import sys from pathlib import Path +from dotenv import load_dotenv + from llama_agent_skills.agent import create_agent, run_agent from llama_agent_skills.config import Config from llama_agent_skills.registry import SkillRegistry @@ -29,6 +31,7 @@ async def _run_interactive(config: Config, registry: SkillRegistry) -> None: skills=skills, model=config.llm_model, api_key=config.llm_api_key, + base_url=config.llm_base_url or None, base_system_prompt=config.base_system_prompt, ) @@ -55,11 +58,13 @@ async def _run_interactive(config: Config, registry: SkillRegistry) -> None: def main() -> None: args = _parse_args() + load_dotenv() config = Config.from_env() if args.model: config = Config( llm_api_key=config.llm_api_key, + llm_base_url=config.llm_base_url, llm_model=args.model, skill_directories=config.skill_directories, base_system_prompt=config.base_system_prompt, diff --git a/src/llama_agent_skills/agent.py b/src/llama_agent_skills/agent.py index 8fd9935..c997254 100644 --- a/src/llama_agent_skills/agent.py +++ b/src/llama_agent_skills/agent.py @@ -2,11 +2,60 @@ from __future__ import annotations from typing import Any, Callable +import tiktoken from llama_index.core.agent.workflow import AgentWorkflow +from llama_index.core.base.llms.types import LLMMetadata from llama_index.llms.openai import OpenAI +from llama_index.llms.openai.utils import ( + CHAT_MODELS, + is_chat_model, + is_function_calling_model, + openai_modelname_to_contextsize, +) from llama_agent_skills.skill import Skill +_DEFAULT_CONTEXT_WINDOW = 131072 +_FALLBACK_ENCODING = "cl100k_base" + + +class _FlexibleOpenAI(OpenAI): + """OpenAI subclass that tolerates unknown model names for custom endpoints.""" + + _custom_context_window: int = _DEFAULT_CONTEXT_WINDOW + + @property + def metadata(self) -> LLMMetadata: + model_name = self._get_model_name() + known_model = model_name in CHAT_MODELS + + try: + context_window = openai_modelname_to_contextsize(model_name) + except ValueError: + context_window = self._custom_context_window + + if known_model: + chat = is_chat_model(model=model_name) + func_calling = is_function_calling_model(model=model_name) + else: + chat = True + func_calling = True + + return LLMMetadata( + context_window=context_window, + num_output=self.max_tokens or -1, + is_chat_model=chat, + is_function_calling_model=func_calling, + model_name=self.model, + ) + + @property + def _tokenizer(self) -> tiktoken.Encoding | None: + try: + return tiktoken.encoding_for_model(self._get_model_name()) + except KeyError: + return tiktoken.get_encoding(_FALLBACK_ENCODING) + def build_system_prompt(base_prompt: str, skills: list[Skill]) -> str: if not skills: @@ -28,6 +77,7 @@ def create_agent( tools: list[Callable[..., Any]] | None = None, model: str = "gpt-4o-mini", api_key: str | None = None, + base_url: str | None = None, base_system_prompt: str = "You are a helpful assistant.", ) -> AgentWorkflow: system_prompt = build_system_prompt(base_system_prompt, skills or []) @@ -35,10 +85,12 @@ def create_agent( llm_kwargs: dict[str, Any] = {"model": model} if api_key: llm_kwargs["api_key"] = api_key + if base_url: + llm_kwargs["api_base"] = base_url return AgentWorkflow.from_tools_or_functions( tools_or_functions=tools or [], - llm=OpenAI(**llm_kwargs), + llm=_FlexibleOpenAI(**llm_kwargs), system_prompt=system_prompt, ) diff --git a/src/llama_agent_skills/config.py b/src/llama_agent_skills/config.py index 965b64a..0c47ce7 100644 --- a/src/llama_agent_skills/config.py +++ b/src/llama_agent_skills/config.py @@ -8,6 +8,7 @@ from pathlib import Path @dataclass(frozen=True) class Config: llm_api_key: str = "" + llm_base_url: str = "" llm_model: str = "gpt-4o-mini" skill_directories: list[Path] = field(default_factory=list) base_system_prompt: str = "You are a helpful assistant." @@ -16,17 +17,24 @@ class Config: def from_env(cls) -> Config: """Create Config from environment variables. - OPENAI_API_KEY, LLM_MODEL, SKILL_DIRECTORIES (comma-separated paths), - BASE_SYSTEM_PROMPT. + OPENAI_API_KEY, OPENAI_BASE_URL, LLM_MODEL / LLM_MODEL_NAME, + SKILL_DIRECTORIES (comma-separated paths), BASE_SYSTEM_PROMPT. """ skill_dirs_raw = os.environ.get("SKILL_DIRECTORIES", "") skill_directories = [ Path(d.strip()) for d in skill_dirs_raw.split(",") if d.strip() ] + model = ( + os.environ.get("LLM_MODEL") + or os.environ.get("LLM_MODEL_NAME") + or "gpt-4o-mini" + ) + return cls( llm_api_key=os.environ.get("OPENAI_API_KEY", ""), - llm_model=os.environ.get("LLM_MODEL", "gpt-4o-mini"), + llm_base_url=os.environ.get("OPENAI_BASE_URL", ""), + llm_model=model, skill_directories=skill_directories, base_system_prompt=os.environ.get( "BASE_SYSTEM_PROMPT", "You are a helpful assistant." diff --git a/uv.lock b/uv.lock index 9aa6721..6b00e72 100644 --- a/uv.lock +++ b/uv.lock @@ -714,13 +714,17 @@ source = { editable = "." } dependencies = [ { name = "llama-index-core" }, { name = "llama-index-llms-openai" }, + { name = "llama-index-llms-openai-like" }, + { name = "python-dotenv" }, { name = "python-frontmatter" }, ] [package.metadata] requires-dist = [ { name = "llama-index-core", specifier = ">=0.12.0" }, - { name = "llama-index-llms-openai", specifier = ">=0.3.0" }, + { name = "llama-index-llms-openai", specifier = ">=0.7.5" }, + { name = "llama-index-llms-openai-like", specifier = ">=0.7.1" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-frontmatter", specifier = ">=1.1.0" }, ] @@ -778,15 +782,28 @@ wheels = [ [[package]] name = "llama-index-llms-openai" -version = "0.7.4" +version = "0.7.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-index-core" }, { name = "openai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/15/1c79fd19e19c38abcb4d6f16bc4c7ce36651bef1efa31973e01603df235b/llama_index_llms_openai-0.7.4.tar.gz", hash = "sha256:e7e078816babe33b29fa17ea0a771cf9589b2e95f1902cd8f1ec283a3a35c675", size = 27323, upload-time = "2026-03-27T15:32:18.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/27/18a7fd0873023aed145332dab5a09b95b298e4fff1c21685eaf22b629d87/llama_index_llms_openai-0.7.5.tar.gz", hash = "sha256:54123e679a7cddc1f2e969f278a4654050730daf84691731a0c53ae14feac3c7", size = 27423, upload-time = "2026-03-30T16:30:33.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/e8/1edce9dad3c7b55e37ca27723f7e8918e71e270bb3e67b49e90abdfaa32f/llama_index_llms_openai-0.7.4-py3-none-any.whl", hash = "sha256:b2cf35ce0d0ecf3209a0406da7383efdbd8f70a1c83eef287e4346c36ea24cba", size = 28378, upload-time = "2026-03-27T15:32:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/63/62/a847e9a94c2f92926c30188259f9f86e019dcc45122bbb222dea35a74c02/llama_index_llms_openai-0.7.5-py3-none-any.whl", hash = "sha256:c302c6386873420df3714c3d538f45379b6de27ab6a531f30c67419b39a538f5", size = 28492, upload-time = "2026-03-30T16:30:32.979Z" }, +] + +[[package]] +name = "llama-index-llms-openai-like" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/9f/0d98d022a08f43d4374998072636ec50a7cb50009bbb9a2761f5b26a78cc/llama_index_llms_openai_like-0.7.1.tar.gz", hash = "sha256:ce7cef3686b1e62d7c08134b4d8ca56706cca816e4c4098eaede33002829a6f9", size = 5177, upload-time = "2026-03-13T16:15:58.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/22/a1e1ec1112c69ca0a379cd72691c36cdbcba78362622ce9a27e5a97965cc/llama_index_llms_openai_like-0.7.1-py3-none-any.whl", hash = "sha256:831f1144077c6f9ea7a62e987b7f2af00310dded3056edca2cb509f70a3e650a", size = 4860, upload-time = "2026-03-13T16:15:59.113Z" }, ] [[package]] @@ -1462,6 +1479,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "python-frontmatter" version = "1.1.0"