content-core 2.0.0__tar.gz → 2.0.2__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.
Files changed (114) hide show
  1. {content_core-2.0.0 → content_core-2.0.2}/CHANGELOG.md +16 -0
  2. {content_core-2.0.0 → content_core-2.0.2}/PKG-INFO +2 -2
  3. {content_core-2.0.0 → content_core-2.0.2}/pyproject.toml +2 -2
  4. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/cli.py +8 -1
  5. content_core-2.0.2/src/content_core/content/summary/core.py +46 -0
  6. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/models.py +2 -2
  7. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/templated_message.py +5 -9
  8. {content_core-2.0.0 → content_core-2.0.2}/uv.lock +5 -5
  9. content_core-2.0.0/src/content_core/content/summary/core.py +0 -20
  10. {content_core-2.0.0 → content_core-2.0.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  11. {content_core-2.0.0 → content_core-2.0.2}/.github/workflows/claude-code-review.yml +0 -0
  12. {content_core-2.0.0 → content_core-2.0.2}/.github/workflows/claude.yml +0 -0
  13. {content_core-2.0.0 → content_core-2.0.2}/.github/workflows/create-tag.yml +0 -0
  14. {content_core-2.0.0 → content_core-2.0.2}/.github/workflows/publish.yml +0 -0
  15. {content_core-2.0.0 → content_core-2.0.2}/.github/workflows/test.yml +0 -0
  16. {content_core-2.0.0 → content_core-2.0.2}/.gitignore +0 -0
  17. {content_core-2.0.0 → content_core-2.0.2}/.python-version +0 -0
  18. {content_core-2.0.0 → content_core-2.0.2}/CLAUDE.md +0 -0
  19. {content_core-2.0.0 → content_core-2.0.2}/CONTRIBUTING.md +0 -0
  20. {content_core-2.0.0 → content_core-2.0.2}/LICENSE +0 -0
  21. {content_core-2.0.0 → content_core-2.0.2}/Makefile +0 -0
  22. {content_core-2.0.0 → content_core-2.0.2}/README.md +0 -0
  23. {content_core-2.0.0 → content_core-2.0.2}/REFACTOR.md +0 -0
  24. {content_core-2.0.0 → content_core-2.0.2}/SKILL.md +0 -0
  25. {content_core-2.0.0 → content_core-2.0.2}/TEST_PLAN.md +0 -0
  26. {content_core-2.0.0 → content_core-2.0.2}/docs/cli.md +0 -0
  27. {content_core-2.0.0 → content_core-2.0.2}/docs/mcp.md +0 -0
  28. {content_core-2.0.0 → content_core-2.0.2}/docs/processors.md +0 -0
  29. {content_core-2.0.0 → content_core-2.0.2}/docs/usage.md +0 -0
  30. {content_core-2.0.0 → content_core-2.0.2}/examples/main.py +0 -0
  31. {content_core-2.0.0 → content_core-2.0.2}/prompts/content/cleanup.jinja +0 -0
  32. {content_core-2.0.0 → content_core-2.0.2}/prompts/content/summarize.jinja +0 -0
  33. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/__init__.py +0 -0
  34. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/common/__init__.py +0 -0
  35. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/common/exceptions.py +0 -0
  36. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/common/retry.py +0 -0
  37. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/common/state.py +0 -0
  38. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/common/types.py +0 -0
  39. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/config.py +0 -0
  40. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/content/__init__.py +0 -0
  41. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/content/extraction/__init__.py +0 -0
  42. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/content/identification/__init__.py +0 -0
  43. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/content/identification/file_detector.py +0 -0
  44. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/content/summary/__init__.py +0 -0
  45. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/extraction.py +0 -0
  46. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/logging.py +0 -0
  47. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/mcp/__init__.py +0 -0
  48. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/mcp/server.py +0 -0
  49. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/__init__.py +0 -0
  50. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/docling.py +0 -0
  51. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/docx.py +0 -0
  52. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/epub.py +0 -0
  53. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/pdf.py +0 -0
  54. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/pptx.py +0 -0
  55. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/document/xlsx.py +0 -0
  56. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/media/__init__.py +0 -0
  57. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/media/audio.py +0 -0
  58. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/media/video.py +0 -0
  59. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/protocol.py +0 -0
  60. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/text.py +0 -0
  61. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/__init__.py +0 -0
  62. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/bs4.py +0 -0
  63. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/crawl4ai.py +0 -0
  64. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/firecrawl.py +0 -0
  65. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/jina.py +0 -0
  66. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/reddit.py +0 -0
  67. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/processors/url/youtube.py +0 -0
  68. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/py.typed +0 -0
  69. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/tools/__init__.py +0 -0
  70. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/tools/extract.py +0 -0
  71. {content_core-2.0.0 → content_core-2.0.2}/src/content_core/tools/summarize.py +0 -0
  72. {content_core-2.0.0 → content_core-2.0.2}/test_coverage_branch_report.md +0 -0
  73. {content_core-2.0.0 → content_core-2.0.2}/tests/conftest.py +0 -0
  74. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/__init__.py +0 -0
  75. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/test_docling.py +0 -0
  76. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/test_media.py +0 -0
  77. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/test_remote.py +0 -0
  78. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/test_url_engines.py +0 -0
  79. {content_core-2.0.0 → content_core-2.0.2}/tests/e2e/test_youtube.py +0 -0
  80. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.docx +0 -0
  81. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.epub +0 -0
  82. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.md +0 -0
  83. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.mp3 +0 -0
  84. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.mp4 +0 -0
  85. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.pdf +0 -0
  86. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.pptx +0 -0
  87. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.txt +0 -0
  88. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file.xlsx +0 -0
  89. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/file_audio.mp3 +0 -0
  90. {content_core-2.0.0 → content_core-2.0.2}/tests/input_content/new_pdf.pdf +0 -0
  91. {content_core-2.0.0 → content_core-2.0.2}/tests/integration/conftest.py +0 -0
  92. {content_core-2.0.0 → content_core-2.0.2}/tests/integration/test_cli_v2.py +0 -0
  93. {content_core-2.0.0 → content_core-2.0.2}/tests/integration/test_extraction.py +0 -0
  94. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_audio_concurrency.py +0 -0
  95. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_cli.py +0 -0
  96. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_config_file.py +0 -0
  97. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_config_v2.py +0 -0
  98. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_docling_extraction.py +0 -0
  99. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_epub_extraction.py +0 -0
  100. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_file_detector.py +0 -0
  101. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_file_detector_critical.py +0 -0
  102. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_file_detector_performance.py +0 -0
  103. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_mcp_v2.py +0 -0
  104. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_media_pipeline.py +0 -0
  105. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_models_v2.py +0 -0
  106. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_office_extraction.py +0 -0
  107. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_pdf_extraction.py +0 -0
  108. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_pdf_helpers.py +0 -0
  109. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_reddit_extraction.py +0 -0
  110. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_retry.py +0 -0
  111. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_routing.py +0 -0
  112. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_text_processing.py +0 -0
  113. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_url_engine_select.py +0 -0
  114. {content_core-2.0.0 → content_core-2.0.2}/tests/unit/test_youtube_parsing.py +0 -0
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.2] - 2026-04-13
11
+
12
+ ### Fixed
13
+ - LLM errors silently swallowed during summarization — errors now propagate with clear messages
14
+ - Thinking models (e.g., Ollama Qwen 3.5) return empty content — now uses `cleaned_content` from Esperanto 2.20.1
15
+ - Default LLM temperature changed from 0 to 0.5 and max_tokens from 600 to 4096 for compatibility with thinking models
16
+ - LLM timeout increased to 120s for local model providers
17
+
18
+ ### Changed
19
+ - Bumped esperanto dependency to >=2.20.1 (thinking model support, trailing slash fix)
20
+
21
+ ## [2.0.1] - 2026-04-13
22
+
23
+ ### Fixed
24
+ - Summarize command fails via `uvx`/`pip install` — Jinja template was not packaged in the wheel
25
+
10
26
  ## [2.0.0] - 2026-04-11
11
27
 
12
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: content-core
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Extract what matters from any media source. Available as Python Library, macOS Service, CLI and MCP Server
5
5
  Author-email: LUIS NOVO <lfnovo@gmail.com>
6
6
  License-File: LICENSE
@@ -10,7 +10,7 @@ Requires-Dist: aiohttp>=3.13
10
10
  Requires-Dist: asciidoc>=10.2.1
11
11
  Requires-Dist: bs4>=0.0.2
12
12
  Requires-Dist: click>=8.3.0
13
- Requires-Dist: esperanto>=2.20.0
13
+ Requires-Dist: esperanto>=2.20.1
14
14
  Requires-Dist: fast-ebook>=0.2.0
15
15
  Requires-Dist: fastmcp>=3.0.0
16
16
  Requires-Dist: firecrawl-py>=4.13.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "content-core"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  description = "Extract what matters from any media source. Available as Python Library, macOS Service, CLI and MCP Server"
5
5
  readme = "README.md"
6
6
  homepage = "https://github.com/lfnovo/content-core"
@@ -11,7 +11,7 @@ requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "aiohttp>=3.13",
13
13
  "bs4>=0.0.2",
14
- "esperanto>=2.20.0",
14
+ "esperanto>=2.20.1",
15
15
  "jinja2>=3.1.6",
16
16
  "langdetect>=1.0.9",
17
17
  "loguru>=0.7.3",
@@ -69,7 +69,14 @@ def summarize(content, context):
69
69
  content = _get_content(content)
70
70
  # If content looks like a URL or file, extract first
71
71
  content = asyncio.run(_maybe_extract(content))
72
- result = asyncio.run(summarize_fn(content, context))
72
+ try:
73
+ result = asyncio.run(summarize_fn(content, context))
74
+ except Exception as e:
75
+ click.echo(f"Error: {e}", err=True)
76
+ sys.exit(1)
77
+ if not result:
78
+ click.echo("Error: Summarization returned no result. Run with --debug for details.", err=True)
79
+ sys.exit(1)
73
80
  click.echo(result)
74
81
 
75
82
 
@@ -0,0 +1,46 @@
1
+ from functools import partial
2
+ from typing import Optional
3
+
4
+ from content_core.config import ContentCoreConfig
5
+ from content_core.models import ModelFactory
6
+ from content_core.templated_message import TemplatedMessageInput, templated_message
7
+
8
+ SUMMARIZE_TEMPLATE = """\
9
+ You are an AI assistant for a personal study platform.
10
+
11
+ In this platform, your user collects various articles and content from the Internet for reference and study.
12
+
13
+ Your role is to summarize the selected content as densely as possible, helping the reader extract maximum value from it without reading the full text.
14
+ Focus solely on the content's value, avoiding unnecessary comments or messages.
15
+
16
+ The summary should be dense, rich in characters, and designed to create a powerful vector representation.
17
+ If the user provided additional context, follow its instructions. Otherwise, summary the whole content.
18
+
19
+ Do not return any acknowledgments or greetings—only the summary.
20
+
21
+ CONTENT:
22
+
23
+ {{ content }}
24
+
25
+ {% if context %}
26
+ CONTEXT:
27
+
28
+ User has provided the aditional context for your task:
29
+ {{context}}
30
+ {% endif %}
31
+
32
+ SUMMARY:"""
33
+
34
+
35
+ async def summarize(
36
+ content: str, context: str, config: Optional[ContentCoreConfig] = None
37
+ ) -> str:
38
+ model = ModelFactory.get_model("summary_model", config=config)
39
+ templated_message_fn = partial(templated_message, model=model)
40
+ response = await templated_message_fn(
41
+ TemplatedMessageInput(
42
+ user_prompt_text=SUMMARIZE_TEMPLATE,
43
+ data={"content": content, "context": context},
44
+ )
45
+ )
46
+ return response
@@ -40,9 +40,9 @@ class ModelFactory:
40
40
  elif model_alias == "summary_model":
41
41
  provider = cfg.llm_provider
42
42
  model_name = cfg.summary_model or cfg.llm_model
43
- return AIFactory.create_language(provider, model_name, config={})
43
+ return AIFactory.create_language(provider, model_name, config={"timeout": 120, "max_tokens": 4096})
44
44
  elif model_alias == "default_model":
45
- return AIFactory.create_language(cfg.llm_provider, cfg.llm_model, config={})
45
+ return AIFactory.create_language(cfg.llm_provider, cfg.llm_model, config={"timeout": 120, "max_tokens": 4096})
46
46
  else:
47
47
  raise ValueError(f"Unknown model alias: {model_alias}")
48
48
 
@@ -5,7 +5,6 @@ from esperanto import LanguageModel
5
5
  from pydantic import BaseModel, Field
6
6
 
7
7
  from content_core.common.retry import retry_llm
8
- from content_core.logging import logger
9
8
  from content_core.models import ModelFactory
10
9
 
11
10
 
@@ -18,9 +17,9 @@ class TemplatedMessageInput(BaseModel):
18
17
  config: Dict = Field(
19
18
  description="The config for the LLM",
20
19
  default={
21
- "temperature": 0,
20
+ "temperature": 0.5,
22
21
  "top_p": 1,
23
- "max_tokens": 600,
22
+ "max_tokens": 4096,
24
23
  },
25
24
  )
26
25
 
@@ -29,7 +28,8 @@ class TemplatedMessageInput(BaseModel):
29
28
  async def _execute_llm_call(model: LanguageModel, msgs: list) -> str:
30
29
  """Internal function to execute LLM call - wrapped with retry logic."""
31
30
  result = await model.achat_complete(msgs)
32
- return result.content
31
+ msg = result.choices[0].message
32
+ return getattr(msg, "cleaned_content", None) or msg.content
33
33
 
34
34
 
35
35
  async def templated_message(
@@ -63,8 +63,4 @@ async def templated_message(
63
63
  ).render(data=input.data)
64
64
  msgs.append({"role": "user", "content": user_prompt})
65
65
 
66
- try:
67
- return await _execute_llm_call(model, msgs)
68
- except Exception as e:
69
- logger.error(f"LLM call failed after retries: {e}")
70
- return None
66
+ return await _execute_llm_call(model, msgs)
@@ -775,7 +775,7 @@ wheels = [
775
775
 
776
776
  [[package]]
777
777
  name = "content-core"
778
- version = "2.0.0"
778
+ version = "2.0.2"
779
779
  source = { editable = "." }
780
780
  dependencies = [
781
781
  { name = "ai-prompter" },
@@ -844,7 +844,7 @@ requires-dist = [
844
844
  { name = "click", specifier = ">=8.3.0" },
845
845
  { name = "crawl4ai", marker = "extra == 'crawl4ai'", specifier = ">=0.7.0" },
846
846
  { name = "docling", marker = "extra == 'docling'", specifier = ">=2.86.0" },
847
- { name = "esperanto", specifier = ">=2.20.0" },
847
+ { name = "esperanto", specifier = ">=2.20.1" },
848
848
  { name = "fast-ebook", specifier = ">=0.2.0" },
849
849
  { name = "fastmcp", specifier = ">=3.0.0" },
850
850
  { name = "firecrawl-py", specifier = ">=4.13.0" },
@@ -1335,15 +1335,15 @@ wheels = [
1335
1335
 
1336
1336
  [[package]]
1337
1337
  name = "esperanto"
1338
- version = "2.20.0"
1338
+ version = "2.20.1"
1339
1339
  source = { registry = "https://pypi.org/simple" }
1340
1340
  dependencies = [
1341
1341
  { name = "httpx" },
1342
1342
  { name = "pydantic" },
1343
1343
  ]
1344
- sdist = { url = "https://files.pythonhosted.org/packages/bb/8c/eba93a2b7b1a4fc6812bcf0a83a69a3376fcaf5831fae04a0ad539baeff6/esperanto-2.20.0.tar.gz", hash = "sha256:53a16c539ed2f83e3400b4e56d5a20b7873df6cb926c1e6696a9611b3be928fb", size = 845084, upload-time = "2026-03-21T14:32:04.589Z" }
1344
+ sdist = { url = "https://files.pythonhosted.org/packages/78/ae/7ff8cd45a2de81236722721417c06843abce3e2c3272edc2b6de1d800d08/esperanto-2.20.1.tar.gz", hash = "sha256:36707c5acdede3ea9c5de621f6dc4256d7e4b461b882cb63fff9e59b420e320c", size = 847075, upload-time = "2026-04-13T14:08:37.586Z" }
1345
1345
  wheels = [
1346
- { url = "https://files.pythonhosted.org/packages/13/83/476dd4d7ee9f1b013ccd66e2518d150e66d3537736aed735b27230af0eb4/esperanto-2.20.0-py3-none-any.whl", hash = "sha256:767f95d08dd29e7d1950f4cc2ec908084e93ec61e945ce9446f001c54b86bb63", size = 203977, upload-time = "2026-03-21T14:32:05.676Z" },
1346
+ { url = "https://files.pythonhosted.org/packages/93/ca/17e01c763e73c168868614746d1d694a10246ece5901b426355cd2c5dd83/esperanto-2.20.1-py3-none-any.whl", hash = "sha256:2cd258b7f1db0d68c06a61effabd7431d23a1a0040a9f413823d1542c521d7ea", size = 204252, upload-time = "2026-04-13T14:08:36.328Z" },
1347
1347
  ]
1348
1348
 
1349
1349
  [[package]]
@@ -1,20 +0,0 @@
1
- from functools import partial
2
- from typing import Optional
3
-
4
- from content_core.config import ContentCoreConfig
5
- from content_core.models import ModelFactory
6
- from content_core.templated_message import TemplatedMessageInput, templated_message
7
-
8
-
9
- async def summarize(
10
- content: str, context: str, config: Optional[ContentCoreConfig] = None
11
- ) -> str:
12
- model = ModelFactory.get_model("summary_model", config=config)
13
- templated_message_fn = partial(templated_message, model=model)
14
- response = await templated_message_fn(
15
- TemplatedMessageInput(
16
- user_prompt_template="content/summarize",
17
- data={"content": content, "context": context},
18
- )
19
- )
20
- return response
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes