llmcall 0.1.0rc1__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.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.1
2
+ Name: llmcall
3
+ Version: 0.1.0rc1
4
+ Summary: A lite abstraction layer for LLM calls
5
+ Home-page: https://github.com/rihoneailabs/llmcall
6
+ License: Apache-2.0
7
+ Keywords: llm,ai,litellm,structure-outputs,openai,pydantic
8
+ Author: Ndamulelo Nemakhavhani
9
+ Author-email: info@rihonegroup.com
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Dist: environs (>=11.2.1,<12.0.0)
19
+ Requires-Dist: litellm (>=1.56.4,<2.0.0)
20
+ Requires-Dist: openai (>=1.58.1,<2.0.0)
21
+ Requires-Dist: pydantic (>=2.0.0)
22
+ Requires-Dist: pydantic-settings (>=2.0.0)
23
+ Requires-Dist: tenacity (>=9.0.0,<10.0.0)
24
+ Project-URL: Repository, https://github.com/rihoneailabs/llmcall.git
25
+ Description-Content-Type: text/markdown
26
+
27
+ # LLMCall
28
+
29
+ A lite abstraction layer for LLM calls.
30
+
31
+ ## Motivation
32
+
33
+ As AI becomes more prevalent in software development, there's a growing need for simple and intuitive APIs for interacting with AI for quick text generation, decision making, and more. This is especially important now that we have structured outputs, which allow us to seamlessly integrate AI into our application flow.
34
+
35
+ `llmcall` provides a minimal, batteries-included interface for common LLM operations without unnecessary complexity.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install llmcall
41
+ ```
42
+
43
+ ## Example Usage
44
+
45
+ ### Generation
46
+
47
+ ```python
48
+ from llmcall import generate, generate_decision
49
+ from pydantic import BaseModel
50
+
51
+ # i. Basic generation
52
+ response = generate("Write a story about a fictional holiday to the sun.")
53
+
54
+ # ii. Structured generation
55
+ class ResponseSchema(BaseModel):
56
+ story: str
57
+ tags: list[str]
58
+
59
+ response: ResponseSchema = generate("Create a rare story about the history of civilisation.", output_schema=schema)
60
+
61
+ # iii. Decision making
62
+ decision = generate_decision(
63
+ "Which is bigger?",
64
+ options=["apple", "berry", "pumpkin"]
65
+ )
66
+ ```
67
+
68
+ ### Extraction
69
+
70
+ ```python
71
+ from llmcall import extract
72
+ from pydantic import BaseModel
73
+
74
+ class ResponseSchema(BaseModel):
75
+ email_subject: str
76
+ email_body: str
77
+ email_topic: str
78
+ email_sentiment: str
79
+
80
+ text = """To whom it may concern,
81
+
82
+ Request for Admission at Harvard University
83
+
84
+ I write to plead with the admission board to consider my application for the 2022/2023 academic year. I am a dedicated student with a passion for computer science and a strong desire to make a difference in the world. I believe that Harvard University is the perfect place for me to achieve my dreams and make a positive impact on society."""
85
+
86
+ response: ResponseSchema = extract(text=text, output_schema=ResponseSchema)
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ Set environment variables:
92
+ - LLMCALL_API_KEY: Your API key
93
+ - LLMCALL_MODEL: Model to use (default: `openai/gpt-4o-2024-08-06`)
94
+
95
+ > **Note**: We recommend using `Open AI` as the model provider due to their robust support for structured outputs. You can use other providers by setting the `LLMCALL_MODEL` or changing the [config](./llmcall/core.py) directly. Any model supported by `LiteLLM` can be used.
96
+
97
+ ## Roadmap
98
+
99
+ - [x] Simple API for generating unstructured text
100
+ - [x] Structured output generation using `Pydantic`
101
+ - [x] Decision making
102
+ - [x] Custom model selection (via `LiteLLM` - See [documentation](https://docs.litellm.ai/docs/providers))
103
+ - [x] Structured text extraction
104
+ - [ ] Structured text extraction from PDF, Docx, etc.
105
+ - [ ] Structured text extraction from Images
106
+ - [ ] Structured text extraction from Websites
107
+
108
+ ## Documentation
109
+
110
+ Please refer to our comprehensive [documentation](./docs/index.md) to learn more about this tool.
111
+
@@ -0,0 +1,84 @@
1
+ # LLMCall
2
+
3
+ A lite abstraction layer for LLM calls.
4
+
5
+ ## Motivation
6
+
7
+ As AI becomes more prevalent in software development, there's a growing need for simple and intuitive APIs for interacting with AI for quick text generation, decision making, and more. This is especially important now that we have structured outputs, which allow us to seamlessly integrate AI into our application flow.
8
+
9
+ `llmcall` provides a minimal, batteries-included interface for common LLM operations without unnecessary complexity.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install llmcall
15
+ ```
16
+
17
+ ## Example Usage
18
+
19
+ ### Generation
20
+
21
+ ```python
22
+ from llmcall import generate, generate_decision
23
+ from pydantic import BaseModel
24
+
25
+ # i. Basic generation
26
+ response = generate("Write a story about a fictional holiday to the sun.")
27
+
28
+ # ii. Structured generation
29
+ class ResponseSchema(BaseModel):
30
+ story: str
31
+ tags: list[str]
32
+
33
+ response: ResponseSchema = generate("Create a rare story about the history of civilisation.", output_schema=schema)
34
+
35
+ # iii. Decision making
36
+ decision = generate_decision(
37
+ "Which is bigger?",
38
+ options=["apple", "berry", "pumpkin"]
39
+ )
40
+ ```
41
+
42
+ ### Extraction
43
+
44
+ ```python
45
+ from llmcall import extract
46
+ from pydantic import BaseModel
47
+
48
+ class ResponseSchema(BaseModel):
49
+ email_subject: str
50
+ email_body: str
51
+ email_topic: str
52
+ email_sentiment: str
53
+
54
+ text = """To whom it may concern,
55
+
56
+ Request for Admission at Harvard University
57
+
58
+ I write to plead with the admission board to consider my application for the 2022/2023 academic year. I am a dedicated student with a passion for computer science and a strong desire to make a difference in the world. I believe that Harvard University is the perfect place for me to achieve my dreams and make a positive impact on society."""
59
+
60
+ response: ResponseSchema = extract(text=text, output_schema=ResponseSchema)
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ Set environment variables:
66
+ - LLMCALL_API_KEY: Your API key
67
+ - LLMCALL_MODEL: Model to use (default: `openai/gpt-4o-2024-08-06`)
68
+
69
+ > **Note**: We recommend using `Open AI` as the model provider due to their robust support for structured outputs. You can use other providers by setting the `LLMCALL_MODEL` or changing the [config](./llmcall/core.py) directly. Any model supported by `LiteLLM` can be used.
70
+
71
+ ## Roadmap
72
+
73
+ - [x] Simple API for generating unstructured text
74
+ - [x] Structured output generation using `Pydantic`
75
+ - [x] Decision making
76
+ - [x] Custom model selection (via `LiteLLM` - See [documentation](https://docs.litellm.ai/docs/providers))
77
+ - [x] Structured text extraction
78
+ - [ ] Structured text extraction from PDF, Docx, etc.
79
+ - [ ] Structured text extraction from Images
80
+ - [ ] Structured text extraction from Websites
81
+
82
+ ## Documentation
83
+
84
+ Please refer to our comprehensive [documentation](./docs/index.md) to learn more about this tool.
@@ -0,0 +1,5 @@
1
+ from llmcall.generate import generate, generate_decision
2
+ from llmcall.extract import extract
3
+ from llmcall.core import LLMConfig, config
4
+
5
+ __all__ = ["generate", "extract", "generate_decision", "config", "LLMConfig"]
@@ -0,0 +1,26 @@
1
+ from typing import Optional
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class ModelConfig(BaseSettings):
6
+ temperature: float = 0.2
7
+ stream: bool = False
8
+ n: Optional[int] = 1
9
+ max_tokens: int = 1024
10
+ num_retries: int = 3
11
+ seed: Optional[int] = 47
12
+
13
+
14
+ class LLMConfig(BaseSettings):
15
+ model_config = SettingsConfigDict(
16
+ case_sensitive=False,
17
+ env_prefix="LLMCALL_",
18
+ extra="ignore",
19
+ )
20
+ api_key: str
21
+ model: str = "openai/gpt-4o-2024-08-06"
22
+ debug: bool = False
23
+ llm: ModelConfig = ModelConfig()
24
+
25
+
26
+ config = LLMConfig()
@@ -0,0 +1,53 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Optional, Union
5
+ from typing_extensions import Annotated
6
+
7
+ from litellm import completion
8
+ from pydantic import BaseModel
9
+
10
+ from llmcall.core import config
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+
15
+ def extract(
16
+ text: Annotated[str, "The unstructured text to extract information from."],
17
+ output_schema: Annotated[BaseModel, "The Pydantic model to use for response structure validation."],
18
+ instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
19
+ ) -> Union[str, BaseModel]:
20
+ """Extract structured information from unstructured text using configured LLM."""
21
+ DEFAULT_SYSTEM_PROMPT = """You are a specialist in organising unstructured data. Given the document below, \
22
+ your task is to extract the requested information as accurately as you can. \
23
+ ###
24
+ <document>{text}</document>
25
+ ###"""
26
+
27
+ if not text:
28
+ raise ValueError("Text cannot be empty.")
29
+
30
+ start = time.perf_counter()
31
+ _logger.info(f"Extracting information from text: {text[:50]}")
32
+
33
+ if instructions:
34
+ messages = [
35
+ {"content": instructions, "role": "system"},
36
+ {"content": text, "role": "user"},
37
+ ]
38
+ else:
39
+ messages = [
40
+ {"content": DEFAULT_SYSTEM_PROMPT.format(text=text), "role": "system"},
41
+ ]
42
+
43
+ response = completion(
44
+ api_key=config.api_key,
45
+ model=config.model,
46
+ messages=messages,
47
+ response_format=output_schema,
48
+ **config.llm.model_dump(),
49
+ )
50
+
51
+ _logger.info(f"Extraction completed in {time.perf_counter() - start:.2f} seconds.")
52
+
53
+ return output_schema.model_validate(json.loads(response.choices[0].message.content), strict=True)
@@ -0,0 +1,20 @@
1
+
2
+ def extract_from_doc(pdf_path, page_num, pdf_type):
3
+ """
4
+ Extracts text from a PDF file.
5
+ """
6
+ raise NotImplementedError
7
+
8
+
9
+ def extract_from_image(image_path):
10
+ """
11
+ Extracts text from an image file.
12
+ """
13
+ raise NotImplementedError
14
+
15
+
16
+ def extract_from_webpage(url):
17
+ """
18
+ Extracts text from a webpage.
19
+ """
20
+ raise NotImplementedError
@@ -0,0 +1,125 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Optional, Union
5
+ from typing_extensions import Annotated
6
+
7
+ import litellm
8
+ from litellm import completion
9
+ from litellm import supports_response_schema
10
+ from pydantic import BaseModel
11
+
12
+ from llmcall.core import config
13
+
14
+ _logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Decision(BaseModel):
18
+ selection: Annotated[str, "The selected option - MUST be one of the provided options."]
19
+ prompt: Optional[str] = None
20
+ options: Optional[list[str]] = None
21
+ reason: Optional[str] = None
22
+
23
+
24
+ def generate(
25
+ prompt: Annotated[str, "The user prompt which tells the model what to generate."],
26
+ output_schema: Annotated[
27
+ Optional[BaseModel], "The Pydantic model to use for response structure validation(optional)"
28
+ ] = None,
29
+ instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
30
+ ) -> Union[str, BaseModel]:
31
+ """Generate content using configured LLM."""
32
+
33
+ if not prompt:
34
+ raise ValueError("Prompt cannot be empty.")
35
+
36
+ DEFAULT_SYSTEM_PROMPT = "Generate content based on the following: <prompt>{prompt}</prompt>. \
37
+ Return only the content with no additional information or comments."
38
+
39
+ _logger.debug(f"Generating content for prompt: {prompt[:20]}..")
40
+ start = time.perf_counter()
41
+
42
+ if output_schema:
43
+ if not supports_response_schema(
44
+ model=config.model.split("/")[1], custom_llm_provider=config.model.split("/")[0]
45
+ ):
46
+ raise ValueError(
47
+ f"Response schema is not supported by the configured model: {config.model}. "
48
+ "Please use a different model(e.g. openai/gpt-4o-2024-08-06) or remove the output schema."
49
+ )
50
+ litellm.enable_json_schema_validation = True
51
+
52
+ response = completion(
53
+ api_key=config.api_key,
54
+ model=config.model,
55
+ messages=[
56
+ {"content": instructions or DEFAULT_SYSTEM_PROMPT, "role": "system"},
57
+ {"content": prompt, "role": "user"},
58
+ ],
59
+ response_format=output_schema,
60
+ **config.llm.model_dump(),
61
+ )
62
+
63
+ _logger.debug(f"Generated content in {time.perf_counter() - start:.2f}s")
64
+
65
+ if output_schema:
66
+ return output_schema.model_validate(json.loads(response.choices[0].message.content), strict=True)
67
+
68
+ return response.choices[0].message.content
69
+
70
+
71
+ def generate_decision(
72
+ prompt: Annotated[str, "The context to consider when making the decision."],
73
+ options: Annotated[list[str], "List of options to choose from."],
74
+ instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
75
+ ) -> Decision:
76
+ """Generate a decision from a list of options."""
77
+
78
+ if not prompt:
79
+ raise ValueError("Prompt cannot be empty.")
80
+
81
+ DEFAULT_SYSTEM_PROMPT = """You are a specialized computer algorithm designed to make decisions in Control Flow scenarios. \
82
+ Your task is to analyze the given context and options, then select the most appropriate option based on the context. Here is the context you need to consider: \
83
+ <context>
84
+ {{CONTEXT}}
85
+ </context>
86
+
87
+ Here are the options you can choose from:
88
+ <options>
89
+ {{OPTIONS}}
90
+ </options>"""
91
+
92
+ _logger.debug(f"Generating decision given options: {options} and prompt: {prompt[:20]}..")
93
+ start = time.perf_counter()
94
+
95
+ if instructions:
96
+ messages = [
97
+ {
98
+ "content": instructions,
99
+ "role": "system",
100
+ },
101
+ {
102
+ "content": "Pick one of the following options: <options>{options}</options>, given the following query:\n<query>{prompt}</query>.",
103
+ "role": "user",
104
+ },
105
+ ]
106
+ else:
107
+ messages = [
108
+ {
109
+ "content": DEFAULT_SYSTEM_PROMPT.strip()
110
+ .replace("{{CONTEXT}}", prompt)
111
+ .replace("{{OPTIONS}}", "\n".join(options)),
112
+ "role": "user",
113
+ },
114
+ ]
115
+
116
+ response = completion(
117
+ api_key=config.api_key,
118
+ model=config.model,
119
+ messages=messages,
120
+ response_format=Decision,
121
+ **config.llm.model_dump(),
122
+ )
123
+
124
+ _logger.debug(f"Generated decision in {time.perf_counter() - start:.2f}s")
125
+ return Decision.model_validate(json.loads(response.choices[0].message.content), strict=True)
@@ -0,0 +1,40 @@
1
+ [tool.poetry]
2
+ name = "llmcall"
3
+ version = "0.1.0rc1"
4
+ description = "A lite abstraction layer for LLM calls"
5
+ authors = ["Ndamulelo Nemakhavhani <info@rihonegroup.com>"]
6
+ license = "Apache-2.0"
7
+ readme = "README.md"
8
+ keywords = ["llm", "ai", "litellm", "structure-outputs", "openai", "pydantic"]
9
+ repository = "https://github.com/rihoneailabs/llmcall.git"
10
+ homepage = "https://github.com/rihoneailabs/llmcall"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: Apache Software License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ ]
20
+
21
+ [tool.poetry.dependencies]
22
+ python = ">=3.11, <4.0"
23
+ pydantic = ">=2.0.0"
24
+ pydantic-settings = ">=2.0.0"
25
+ openai = "^1.58.1"
26
+ litellm = "^1.56.4"
27
+ environs = "^11.2.1"
28
+ tenacity = "^9.0.0"
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ pytest = "^8.3.4"
32
+ black = "^24.10.0"
33
+ isort = "^5.13.2"
34
+ mypy = "^1.14.0"
35
+ tox = "^4.23.2"
36
+ pytest-cov = "^6.0.0"
37
+
38
+ [build-system]
39
+ requires = ["poetry-core"]
40
+ build-backend = "poetry.core.masonry.api"