django-spire 0.19.5__py3-none-any.whl → 0.20.1__py3-none-any.whl

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 (39) hide show
  1. django_spire/ai/chat/intelligence/decoders/tools.py +7 -2
  2. django_spire/ai/chat/intelligence/workflows/chat_workflow.py +22 -22
  3. django_spire/consts.py +1 -1
  4. django_spire/core/migrations/0001_initial.py +26 -0
  5. django_spire/core/migrations/__init__.py +0 -0
  6. django_spire/core/tags/__init__.py +0 -0
  7. django_spire/core/tags/intelligence/__init__.py +0 -0
  8. django_spire/core/tags/intelligence/tag_set_bot.py +41 -0
  9. django_spire/core/tags/mixins.py +61 -0
  10. django_spire/core/tags/models.py +38 -0
  11. django_spire/core/tags/querysets.py +9 -0
  12. django_spire/core/tags/tests/__init__.py +0 -0
  13. django_spire/core/tags/tests/test_intelligence.py +28 -0
  14. django_spire/core/tags/tests/test_tags.py +102 -0
  15. django_spire/core/tags/tools.py +20 -0
  16. django_spire/knowledge/collection/admin.py +20 -1
  17. django_spire/knowledge/collection/models.py +6 -1
  18. django_spire/knowledge/collection/services/service.py +2 -0
  19. django_spire/knowledge/collection/services/tag_service.py +52 -0
  20. django_spire/knowledge/collection/views/form_views.py +2 -0
  21. django_spire/knowledge/entry/admin.py +18 -2
  22. django_spire/knowledge/entry/models.py +6 -1
  23. django_spire/knowledge/entry/services/service.py +2 -0
  24. django_spire/knowledge/entry/services/tag_service.py +34 -0
  25. django_spire/knowledge/entry/version/urls/json_urls.py +5 -1
  26. django_spire/knowledge/entry/version/views/json_views.py +9 -0
  27. django_spire/knowledge/intelligence/decoders/collection_decoder.py +4 -4
  28. django_spire/knowledge/intelligence/decoders/entry_decoder.py +2 -2
  29. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +9 -3
  30. django_spire/knowledge/migrations/0008_collection_tags_entry_tags.py +24 -0
  31. django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +16 -0
  32. django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/{detail_container.html → editor_container.html} +6 -0
  33. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +27 -10
  34. django_spire/settings.py +1 -0
  35. {django_spire-0.19.5.dist-info → django_spire-0.20.1.dist-info}/METADATA +2 -2
  36. {django_spire-0.19.5.dist-info → django_spire-0.20.1.dist-info}/RECORD +39 -24
  37. {django_spire-0.19.5.dist-info → django_spire-0.20.1.dist-info}/WHEEL +0 -0
  38. {django_spire-0.19.5.dist-info → django_spire-0.20.1.dist-info}/licenses/LICENSE.md +0 -0
  39. {django_spire-0.19.5.dist-info → django_spire-0.20.1.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,8 @@ from typing import Callable, TYPE_CHECKING
5
5
  from dandy import Decoder
6
6
 
7
7
  from django_spire.auth.controller.controller import AppAuthController
8
+ from django_spire.conf import settings
9
+ from django_spire.core.utils import get_callable_from_module_string_and_validate_arguments
8
10
  from django_spire.knowledge.intelligence.workflows.knowledge_workflow import knowledge_search_workflow
9
11
 
10
12
  if TYPE_CHECKING:
@@ -20,8 +22,11 @@ def generate_intent_decoder(
20
22
  if AppAuthController(app_name='knowledge', request=request).can_view():
21
23
  intent_dict['The user is looking for information or knowledge on something.'] = knowledge_search_workflow
22
24
 
23
- if default_callable is not None:
24
- intent_dict['None of the above choices match the user\'s intent'] = default_callable
25
+ if settings.AI_CHAT_DEFAULT_CALLABLE is not None:
26
+ intent_dict['None of the above choices match the user\'s intent'] = get_callable_from_module_string_and_validate_arguments(
27
+ settings.AI_CHAT_DEFAULT_CALLABLE,
28
+ ['request', 'user_input', 'message_history']
29
+ )
25
30
 
26
31
  return Decoder(
27
32
  mapping_keys_description='Intent of the User\'s Request',
@@ -8,25 +8,15 @@ from dandy.recorder import recorder_to_html_file
8
8
  from django_spire.ai.chat.intelligence.decoders.tools import generate_intent_decoder
9
9
  from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
10
10
  from django_spire.ai.decorators import log_ai_interaction_from_recorder
11
+ from django_spire.conf import settings
12
+ from django_spire.core.utils import get_callable_from_module_string_and_validate_arguments
11
13
 
12
14
  if TYPE_CHECKING:
13
15
  from dandy.llm.request.message import MessageHistory
14
16
  from django.core.handlers.wsgi import WSGIRequest
15
17
 
16
18
 
17
- def SpireChatWorkflow(
18
- request: WSGIRequest,
19
- user_input: str,
20
- message_history: MessageHistory | None = None
21
- ) -> BaseMessageIntel:
22
- return chat_workflow(
23
- request=request,
24
- user_input=user_input,
25
- message_history=message_history
26
- )
27
-
28
-
29
- def default_chat_response(
19
+ def default_chat_callable(
30
20
  request: WSGIRequest,
31
21
  user_input: str,
32
22
  message_history: MessageHistory | None = None
@@ -45,13 +35,6 @@ def chat_workflow(
45
35
  user_input: str,
46
36
  message_history: MessageHistory | None = None
47
37
  ) -> BaseMessageIntel:
48
- intent_decoder = generate_intent_decoder(
49
- request=request,
50
- default_callable=default_chat_response,
51
- )
52
-
53
- intent_process = intent_decoder.process(user_input, max_return_values=1)[0]
54
-
55
38
  @log_ai_interaction_from_recorder(request.user)
56
39
  def run_workflow_process(callable_: Callable) -> BaseMessageIntel | None:
57
40
  return callable_(
@@ -60,11 +43,28 @@ def chat_workflow(
60
43
  message_history=message_history,
61
44
  )
62
45
 
63
- message_intel = run_workflow_process(intent_process)
46
+ if settings.AI_CHAT_CALLABLE is not None:
47
+ chat_callable = get_callable_from_module_string_and_validate_arguments(
48
+ settings.AI_CHAT_CALLABLE,
49
+ ['request', 'user_input', 'message_history']
50
+ )
51
+
52
+ message_intel = run_workflow_process(chat_callable)
53
+
54
+ else:
55
+
56
+ intent_decoder = generate_intent_decoder(
57
+ request=request,
58
+ default_callable=default_chat_callable,
59
+ )
60
+
61
+ intent_process = intent_decoder.process(user_input, max_return_values=1)[0]
62
+
63
+ message_intel = run_workflow_process(intent_process)
64
64
 
65
65
  if not isinstance(message_intel, BaseMessageIntel):
66
66
  if message_intel is None:
67
- return default_chat_response(
67
+ return default_chat_callable(
68
68
  request=request,
69
69
  user_input=user_input,
70
70
  message_history=message_history
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.19.5'
1
+ __VERSION__ = '0.20.1'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -0,0 +1,26 @@
1
+ # Generated by Django 5.2.7 on 2025-11-09 05:02
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='Tag',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('name', models.SlugField(max_length=128, unique=True)),
19
+ ],
20
+ options={
21
+ 'verbose_name': 'Tag',
22
+ 'verbose_name_plural': 'Tags',
23
+ 'db_table': 'django_spire_core_tag',
24
+ },
25
+ ),
26
+ ]
File without changes
File without changes
File without changes
@@ -0,0 +1,41 @@
1
+ from dandy import Bot, Prompt, BaseListIntel
2
+ from django.utils.text import slugify
3
+
4
+
5
+ class TagsIntel(BaseListIntel[list[str]]):
6
+ tags: list[str]
7
+
8
+
9
+ class TagSetBot(Bot):
10
+ llm_role = 'Tag Identifier'
11
+ llm_task = 'Read through the provided content and return a list of tags.'
12
+ llm_guidelines = (
13
+ Prompt()
14
+ .list([
15
+ 'Make sure to have enough tags to properly cover all the provided content.',
16
+ 'Focus on tagging the words in the content',
17
+ 'Only add additional words that are very relevant to the content.'
18
+ 'Use spaces to separate words in tags',
19
+ 'Include known acronyms along with the full tags.',
20
+ ])
21
+ )
22
+
23
+ def process(self, content: Prompt | str) -> set[str]:
24
+ tags_intel = self.llm.prompt_to_intel(
25
+ prompt=(
26
+ Prompt()
27
+ .heading('Content to be Tagged:')
28
+ .line_break()
29
+ .text(content)
30
+ ),
31
+ intel_class=TagsIntel
32
+ )
33
+
34
+ for tag in tags_intel:
35
+ word_segments = tag.split(" ")
36
+
37
+ if len(word_segments) > 1:
38
+ for word_segment in word_segments:
39
+ tags_intel.append(word_segment)
40
+
41
+ return {slugify(tag) for tag in tags_intel}
@@ -0,0 +1,61 @@
1
+ from django.db import models
2
+ from django.db.models import QuerySet
3
+
4
+ from django_spire.core.tags.models import Tag
5
+ from django_spire.core.tags import tools
6
+
7
+
8
+ class TagsModelMixin(models.Model):
9
+ tags = models.ManyToManyField(Tag, related_name='+', null=True, blank=True, editable=False)
10
+
11
+ class Meta:
12
+ abstract = True
13
+
14
+ @property
15
+ def tag_set(self) -> set[str]:
16
+ return set(self.tags.values_list('name', flat=True))
17
+
18
+ @property
19
+ def tag_set_simplified(self) -> set[str]:
20
+ simplified_tag_words = []
21
+
22
+ for tag in self.tag_set:
23
+ tag_words = tag.split('-')
24
+ simplified_tag_words.extend(tag_words)
25
+
26
+ return set(simplified_tag_words)
27
+
28
+ def add_tags_from_tag_set(self, tag_set: set[str]):
29
+ self._update_global_tags_from_set(tag_set)
30
+
31
+ self.tags.add(*Tag.objects.in_tag_set(tag_set))
32
+
33
+ def clear_tags(self):
34
+ self.tags.clear()
35
+
36
+ def get_matching_tags_from_tag_set(self, tag_set: set[str]) -> QuerySet:
37
+ return self.tags.in_tag_set(tag_set)
38
+
39
+ def get_matching_percentage_of_tag_set(self, tag_set: set[str]) -> float:
40
+ return tools.get_matching_a_percentage_from_tag_sets(tag_set, self.tag_set)
41
+
42
+ def get_matching_percentage_of_model_tags_from_tag_set(self, tag_set: set[str]) -> float:
43
+ return tools.get_matching_b_percentage_from_tag_sets(tag_set, self.tag_set)
44
+
45
+ def has_tags_in_tag_set(self, tag_set: set[str]) -> bool:
46
+ return self.tag_set.issuperset(tag_set)
47
+
48
+ def remove_tags_by_tag_set(self, tag_set: set[str]):
49
+ tag_objects = Tag.objects.in_tag_set(tag_set)
50
+
51
+ self.tags.remove(*tag_objects)
52
+
53
+ def set_tags_from_tag_set(self, tag_set: set[str]):
54
+ self._update_global_tags_from_set(tag_set)
55
+
56
+ tag_objects = Tag.objects.in_tag_set(tag_set)
57
+ self.tags.set(tag_objects)
58
+
59
+ @staticmethod
60
+ def _update_global_tags_from_set(tag_set: set[str]):
61
+ Tag.add_tags([Tag(name=tag) for tag in tag_set])
@@ -0,0 +1,38 @@
1
+ from typing import Sequence, Self
2
+
3
+ from django.db import models
4
+ from django.utils.text import slugify
5
+
6
+ from django_spire.core.tags.querysets import TagQuerySet
7
+
8
+
9
+ class Tag(models.Model):
10
+ name = models.SlugField(
11
+ max_length=128,
12
+ unique=True
13
+ )
14
+
15
+ objects = TagQuerySet.as_manager()
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ name = kwargs.get('name')
19
+ if name:
20
+ kwargs['name'] = slugify(name)
21
+
22
+ super().__init__(*args, **kwargs)
23
+
24
+ def __str__(self):
25
+ return self.name
26
+
27
+ @classmethod
28
+ def add_tags(cls, tags: Sequence[Self]):
29
+ cls.objects.bulk_create(
30
+ tags,
31
+ ignore_conflicts=True
32
+ )
33
+
34
+ class Meta:
35
+ db_table = 'django_spire_core_tag'
36
+ verbose_name = 'Tag'
37
+ verbose_name_plural = 'Tags'
38
+
@@ -0,0 +1,9 @@
1
+ from django.db.models import QuerySet
2
+ from django.utils.text import slugify
3
+
4
+
5
+ class TagQuerySet(QuerySet):
6
+ def in_tag_set(self, tag_set: set[str]):
7
+ slugified_tag_set = set(map(slugify, tag_set))
8
+
9
+ return self.filter(name__in=slugified_tag_set)
File without changes
@@ -0,0 +1,28 @@
1
+ from unittest import TestCase
2
+
3
+ from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
4
+
5
+ TEST_INPUT = """
6
+ I love reading books about science fiction, fantasy, and adventure, especially ones
7
+ that feature artificial intelligence, machine learning, and natural language processing.
8
+ Some of my favorite authors include Isaac Asimov, J.R.R. Tolkien, and Neil Gaiman.
9
+ I'm also interested in learning more about data science, programming languages like Python
10
+ and Java, and emerging technologies like blockchain and the Internet of Things (IoT).
11
+ Can you recommend some books or resources that align with these interests?
12
+ """
13
+
14
+
15
+ class TestTagIntelligence(TestCase):
16
+ def setUp(self):
17
+ pass
18
+
19
+ def test_tag_set_bot(self):
20
+ tag_set = TagSetBot().process(TEST_INPUT)
21
+
22
+ self.assertIn('science', tag_set)
23
+ self.assertIn('artificial', tag_set)
24
+ self.assertIn('fantasy', tag_set)
25
+
26
+ self.assertNotIn('camping', tag_set)
27
+ self.assertNotIn('art', tag_set)
28
+ self.assertNotIn('hate', tag_set)
@@ -0,0 +1,102 @@
1
+ from django.test import TestCase
2
+
3
+ from django_spire.core.tags.models import Tag
4
+ from django_spire.knowledge.collection.models import Collection
5
+
6
+
7
+ class TestTags(TestCase):
8
+ def setUp(self):
9
+ self.collection = Collection.objects.create(
10
+ name='Test Collection', description='Test Collection Description'
11
+ )
12
+
13
+ self.tag_set_a = {'apple', 'banana', 'orange'}
14
+ self.tag_set_b = {'grape', 'kiwi', 'pear'}
15
+ self.tag_set_c = {'lemon', 'lime', 'mango'}
16
+
17
+ self.mixed_tag_set = {'apple', 'grape', 'lemon'}
18
+
19
+ self.dirty_tag_set = {'HaT~', 'four* tacos', '123 hoop!!'}
20
+
21
+ self.collection.add_tags_from_tag_set(self.tag_set_a)
22
+ self.collection.add_tags_from_tag_set(self.tag_set_b)
23
+ self.collection.add_tags_from_tag_set(self.tag_set_c)
24
+
25
+ self.collection.add_tags_from_tag_set(self.mixed_tag_set)
26
+
27
+ self.collection.add_tags_from_tag_set(self.dirty_tag_set)
28
+
29
+ def test_tag_removal(self):
30
+ self.assertIn('apple', self.collection.tag_set)
31
+
32
+ self.collection.remove_tags_by_tag_set(self.mixed_tag_set)
33
+
34
+ self.assertNotIn('lemon', self.collection.tag_set)
35
+
36
+ self.assertIn('kiwi', self.collection.tag_set)
37
+
38
+ def test_tag_add_and_set(self):
39
+ self.collection.set_tags_from_tag_set(self.mixed_tag_set)
40
+
41
+ self.assertIn('lemon', self.collection.tag_set)
42
+
43
+ self.assertNotIn('banana', self.collection.tag_set)
44
+
45
+ def test_tag_count(self):
46
+ self.assertEqual(Tag.objects.count(), 12)
47
+ self.assertEqual(self.collection.tags.count(), 12)
48
+
49
+ def test_tag_removal(self):
50
+ self.collection.remove_tags_by_tag_set(self.tag_set_a)
51
+
52
+ self.assertEqual(self.collection.tags.count(), 9)
53
+
54
+ def test_tag_bool_methods(self):
55
+ self.collection.remove_tags_by_tag_set(self.tag_set_b)
56
+
57
+ self.assertTrue(self.collection.has_tags_in_tag_set(self.tag_set_c))
58
+ self.assertFalse(self.collection.has_tags_in_tag_set(self.mixed_tag_set))
59
+
60
+ def test_tag_matching_methods(self):
61
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_a)), 3)
62
+
63
+ self.collection.remove_tags_by_tag_set(self.tag_set_a)
64
+
65
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_a)), 0)
66
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_b)), 3)
67
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.mixed_tag_set)), 2)
68
+
69
+ self.collection.remove_tags_by_tag_set(self.tag_set_b)
70
+
71
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.tag_set_b)), 0)
72
+
73
+ self.assertEqual(len(self.collection.get_matching_tags_from_tag_set(self.mixed_tag_set)), 1)
74
+
75
+ def test_tag_metric_methods(self):
76
+ self.assertAlmostEqual(
77
+ self.collection.get_matching_percentage_of_tag_set(self.tag_set_a),
78
+ 1.00,
79
+ 2
80
+ )
81
+
82
+ self.assertAlmostEqual(
83
+ self.collection.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
84
+ 0.25,
85
+ 2
86
+ )
87
+
88
+ self.collection.remove_tags_by_tag_set(self.mixed_tag_set)
89
+
90
+ self.assertAlmostEqual(
91
+ self.collection.get_matching_percentage_of_tag_set(self.tag_set_a),
92
+ 0.666,
93
+ 2
94
+ )
95
+
96
+
97
+ self.assertAlmostEqual(
98
+ self.collection.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
99
+ 0.222,
100
+ 2
101
+ )
102
+
@@ -0,0 +1,20 @@
1
+ def get_matching_tag_set_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> set[str]:
2
+ return tag_set_a.intersection(tag_set_b)
3
+
4
+
5
+ def get_matching_count_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> int:
6
+ return len(get_matching_tag_set_from_tag_sets(tag_set_a, tag_set_b))
7
+
8
+
9
+ def get_matching_a_percentage_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> float:
10
+ try:
11
+ return get_matching_count_from_tag_sets(tag_set_a, tag_set_b) / len(tag_set_a)
12
+ except ZeroDivisionError:
13
+ return 0.0
14
+
15
+
16
+ def get_matching_b_percentage_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> float:
17
+ try:
18
+ return get_matching_count_from_tag_sets(tag_set_a, tag_set_b) / len(tag_set_b)
19
+ except ZeroDivisionError:
20
+ return 0.0
@@ -1,18 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from django.contrib import admin
4
+ from django.db.models import QuerySet
5
+ from django.contrib import messages
4
6
 
5
7
  from .models import Collection, CollectionGroup
6
8
 
7
9
 
8
10
  @admin.register(Collection)
9
11
  class CollectionAdmin(admin.ModelAdmin):
10
- list_display = ['id', 'name', 'parent', 'is_deleted']
12
+ list_display = ['id', 'name', 'parent', 'is_deleted', 'tag_count']
11
13
  list_select_related = ['parent']
12
14
  list_filter = ['is_deleted', 'is_active']
13
15
  search_fields = ['id', 'name', 'description', 'parent__name']
14
16
  ordering = ['name']
15
17
  autocomplete_fields = ['parent']
18
+ actions = ['set_tags_for_collections']
19
+
20
+ @admin.action(description="Set Tags for Collections (Allow 5 Seconds Per)")
21
+ def set_tags_for_collections(self, request, queryset: QuerySet[Collection]):
22
+ processed = 0
23
+ for collection in queryset:
24
+ collection.services.tag.process_and_set_tags()
25
+ processed += 1
26
+
27
+ messages.success(request, f'Successfully processed {processed} collections.')
28
+
29
+ def tag_count(self, collection: Collection):
30
+ return collection.tags.count()
31
+
32
+ tag_count.short_description = 'Tags'
16
33
 
17
34
 
18
35
  @admin.register(CollectionGroup)
@@ -20,3 +37,5 @@ class CollectionGroupAdmin(admin.ModelAdmin):
20
37
  list_display = ['id', 'collection', 'auth_group']
21
38
  list_select_related = ['collection', 'auth_group']
22
39
  search_fields = ['id', 'collection__name', 'auth_group__name']
40
+
41
+
@@ -5,6 +5,7 @@ from django.db import models
5
5
  from django_spire.auth.group.models import AuthGroup
6
6
  from django_spire.contrib import Breadcrumbs
7
7
  from django_spire.contrib.ordering.mixins import OrderingModelMixin
8
+ from django_spire.core.tags.mixins import TagsModelMixin
8
9
  from django_spire.contrib.utils import truncate_string
9
10
  from django_spire.history.mixins import HistoryModelMixin
10
11
  from django_spire.knowledge.collection.querysets import CollectionQuerySet
@@ -12,7 +13,11 @@ from django_spire.knowledge.collection.services.service import CollectionGroupSe
12
13
  CollectionService
13
14
 
14
15
 
15
- class Collection(HistoryModelMixin, OrderingModelMixin):
16
+ class Collection(
17
+ HistoryModelMixin,
18
+ OrderingModelMixin,
19
+ TagsModelMixin,
20
+ ):
16
21
  parent = models.ForeignKey(
17
22
  'self',
18
23
  on_delete=models.CASCADE,
@@ -10,6 +10,7 @@ from django_spire.knowledge.collection.services.ordering_service import \
10
10
  CollectionOrderingService
11
11
  from django_spire.knowledge.collection.services.processor_service import \
12
12
  CollectionProcessorService
13
+ from django_spire.knowledge.collection.services.tag_service import CollectionTagService
13
14
  from django_spire.knowledge.collection.services.transformation_service import \
14
15
  CollectionTransformationService
15
16
 
@@ -22,6 +23,7 @@ class CollectionService(BaseDjangoModelService['Collection']):
22
23
 
23
24
  ordering = CollectionOrderingService()
24
25
  processor = CollectionProcessorService()
26
+ tag = CollectionTagService()
25
27
  transformation = CollectionTransformationService()
26
28
 
27
29
  def save_model_obj(self, **field_data) -> tuple[Collection, bool]:
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from dandy import Prompt
6
+
7
+ from django_spire.contrib.service import BaseDjangoModelService
8
+ from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
9
+
10
+ if TYPE_CHECKING:
11
+ from django_spire.knowledge.collection.models import Collection
12
+
13
+
14
+ class CollectionTagService(BaseDjangoModelService['Collection']):
15
+ obj: Collection
16
+
17
+ def process_and_set_tags(self):
18
+ collection_prompt = Prompt()
19
+
20
+ collection_prompt.sub_heading(self.obj.name)
21
+ collection_prompt.text(self.obj.description)
22
+
23
+ tag_set = TagSetBot().process(
24
+ content=collection_prompt
25
+ )
26
+
27
+ self.obj.set_tags_from_tag_set(
28
+ tag_set=tag_set,
29
+ )
30
+
31
+ def get_aggregated_tag_set(self):
32
+ tag_set = self.obj.tag_set
33
+
34
+ for collection in self.obj.children.active():
35
+ tag_set.update(collection.services.tag.get_aggregated_tag_set())
36
+
37
+ for entry in self.obj.entries.active():
38
+ tag_set.update(entry.tag_set)
39
+
40
+ return tag_set
41
+
42
+ def get_aggregated_tag_set_simplified(self):
43
+ tag_set = self.obj.tag_set_simplified
44
+
45
+ for collection in self.obj.children.active():
46
+ tag_set.update(collection.services.tag.get_aggregated_tag_set())
47
+
48
+ for entry in self.obj.entries.active():
49
+ tag_set.update(entry.tag_set_simplified)
50
+
51
+ return tag_set
52
+
@@ -61,6 +61,8 @@ def form_view(
61
61
  collection=collection,
62
62
  )
63
63
 
64
+ collection.services.tag.process_and_set_tags()
65
+
64
66
  if collection.parent_id:
65
67
  return_url = reverse(
66
68
  'django_spire:knowledge:collection:page:top_level',
@@ -1,4 +1,5 @@
1
- from django.contrib import admin
1
+ from django.contrib import admin, messages
2
+ from django.db.models import QuerySet
2
3
  from django.urls import reverse
3
4
  from django.utils.html import format_html
4
5
  from django.utils.http import urlencode
@@ -8,12 +9,13 @@ from django_spire.knowledge.entry.models import Entry
8
9
 
9
10
  @admin.register(Entry)
10
11
  class EntryAdmin(admin.ModelAdmin):
11
- list_display = ['name', 'current_version_link', 'collection', 'is_deleted']
12
+ list_display = ['name', 'current_version_link', 'collection', 'is_deleted', 'tag_count']
12
13
  list_select_related = ['collection', 'current_version']
13
14
  list_filter = ['is_deleted', 'is_active']
14
15
  search_fields = ['name', 'collection__name']
15
16
  ordering = ['name']
16
17
  autocomplete_fields = ['collection', 'current_version']
18
+ actions = ['set_tags_for_entries']
17
19
 
18
20
  def current_version_link(self, entry: Entry) -> str:
19
21
  url = (
@@ -26,3 +28,17 @@ class EntryAdmin(admin.ModelAdmin):
26
28
 
27
29
  current_version_link.short_description = 'Current Version'
28
30
  current_version_link.allow_tags = True
31
+
32
+ @admin.action(description="Set Tags for Entries (Allow 5 Seconds Per)")
33
+ def set_tags_for_entries(self, request, queryset: QuerySet[Entry]):
34
+ processed = 0
35
+ for entry in queryset:
36
+ entry.services.tag.process_and_set_tags()
37
+ processed += 1
38
+
39
+ messages.success(request, f'Successfully processed {processed} entries.')
40
+
41
+ def tag_count(self, entry: Entry):
42
+ return entry.tags.count()
43
+
44
+ tag_count.short_description = 'Tags'
@@ -3,6 +3,7 @@ from django.urls import reverse
3
3
 
4
4
  from django_spire.contrib import Breadcrumbs
5
5
  from django_spire.contrib.ordering.mixins import OrderingModelMixin
6
+ from django_spire.core.tags.mixins import TagsModelMixin
6
7
  from django_spire.contrib.utils import truncate_string
7
8
  from django_spire.history.mixins import HistoryModelMixin
8
9
  from django_spire.knowledge.collection.models import Collection
@@ -11,7 +12,11 @@ from django_spire.knowledge.entry.services.service import EntryService
11
12
  from django_spire.knowledge.entry.version.models import EntryVersion
12
13
 
13
14
 
14
- class Entry(HistoryModelMixin, OrderingModelMixin):
15
+ class Entry(
16
+ HistoryModelMixin,
17
+ OrderingModelMixin,
18
+ TagsModelMixin
19
+ ):
15
20
  collection = models.ForeignKey(
16
21
  Collection,
17
22
  on_delete=models.CASCADE,
@@ -10,6 +10,7 @@ from django_spire.knowledge.entry.services.automation_service import \
10
10
  from django_spire.knowledge.entry.services.factory_service import EntryFactoryService
11
11
  from django_spire.knowledge.entry.services.processor_service import \
12
12
  EntryProcessorService
13
+ from django_spire.knowledge.entry.services.tag_service import EntryTagService
13
14
  from django_spire.knowledge.entry.services.tool_service import EntryToolService
14
15
  from django_spire.knowledge.entry.services.transformation_services import \
15
16
  EntryTransformationService
@@ -26,6 +27,7 @@ class EntryService(BaseDjangoModelService['Entry']):
26
27
  factory = EntryFactoryService()
27
28
  ordering = OrderingService()
28
29
  processor = EntryProcessorService()
30
+ tag = EntryTagService()
29
31
  tool = EntryToolService()
30
32
  transformation = EntryTransformationService()
31
33
 
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from dandy import Prompt
6
+
7
+ from django_spire.contrib.service import BaseDjangoModelService
8
+ from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
9
+
10
+ if TYPE_CHECKING:
11
+ from django_spire.knowledge.entry.models import Entry
12
+
13
+
14
+
15
+ class EntryTagService(BaseDjangoModelService['Entry']):
16
+ obj: Entry
17
+
18
+ def process_and_set_tags(self):
19
+ entry_prompt = Prompt()
20
+
21
+ entry_prompt.text(self.obj.name)
22
+
23
+ for version_block in self.obj.current_version.blocks.all():
24
+ entry_prompt.text(f'{version_block.render_to_text()}')
25
+
26
+ tag_set = TagSetBot().process(
27
+ content=entry_prompt
28
+ )
29
+
30
+ self.obj.set_tags_from_tag_set(
31
+ tag_set=tag_set,
32
+ )
33
+
34
+
@@ -9,8 +9,12 @@ app_name = 'json'
9
9
  urlpatterns = [
10
10
  path(
11
11
  '<int:pk>/update_blocks/',
12
-
13
12
  json_views.update_blocks_view,
14
13
  name='update_blocks',
15
14
  ),
15
+ path(
16
+ '<int:pk>/update_entry_from_version/',
17
+ json_views.update_entry_from_version_view,
18
+ name='update_entry_from_version',
19
+ ),
16
20
  ]
@@ -21,3 +21,12 @@ def update_blocks_view(request: WSGIRequest, pk: int) -> JsonResponse:
21
21
  entry_version.services.processor.add_update_delete_blocks(block_data_list)
22
22
 
23
23
  return JsonResponse({'type': 'success'})
24
+
25
+ @valid_ajax_request_required
26
+ @AppAuthController('knowledge').permission_required('can_change')
27
+ def update_entry_from_version_view(request: WSGIRequest, pk: int) -> JsonResponse:
28
+ entry_version = get_object_or_404(EntryVersion, pk=pk)
29
+
30
+ entry_version.entry.services.tag.process_and_set_tags()
31
+
32
+ return JsonResponse({'type': 'success'})
@@ -7,13 +7,13 @@ from django_spire.knowledge.collection.models import Collection
7
7
 
8
8
  def get_collection_decoder() -> Decoder:
9
9
  class CollectionDecoder(Decoder):
10
- mapping_keys_description = 'Knowledge Collection Titles'
10
+ mapping_keys_description = 'Knowledge Collections Tags'
11
11
  mapping = {
12
12
  **{
13
- f'{collection.name}: {collection.description}': collection
13
+ f'{collection.services.tag.get_aggregated_tag_set_simplified()}': collection
14
14
  for collection in Collection.objects.all().annotate_entry_count()
15
15
  },
16
- 'No Matching Knowledge Collection Titles': None,
16
+ 'No Matching Knowledge Collection Tags': None,
17
17
  }
18
18
 
19
- return CollectionDecoder()
19
+ return CollectionDecoder()
@@ -7,10 +7,10 @@ from django_spire.knowledge.collection.models import Collection
7
7
 
8
8
  def get_entry_decoder(collection: Collection) -> Decoder:
9
9
  class EntryDecoder(Decoder):
10
- mapping_keys_description = 'Knowledge Entries'
10
+ mapping_keys_description = 'Knowledge Entries Tags'
11
11
  mapping = {
12
12
  **{
13
- entry.name: entry
13
+ f'{entry.tag_set}': entry
14
14
  for entry in collection.entries.all()
15
15
  },
16
16
  'No Matching Knowledge Entries': None
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from django.core.handlers.wsgi import WSGIRequest
6
+
7
+ from django_spire.ai.chat.message_intel import DefaultMessageIntel, BaseMessageIntel
6
8
  from django_spire.knowledge.intelligence.bots.entry_search_llm_bot import EntrySearchBot
7
9
  from django_spire.knowledge.intelligence.intel.entry_intel import EntriesIntel
8
10
  from django_spire.knowledge.intelligence.intel.message_intel import KnowledgeMessageIntel
@@ -13,17 +15,21 @@ if TYPE_CHECKING:
13
15
  from django.core.handlers.wsgi import WSGIRequest
14
16
  from dandy.llm.request.message import MessageHistory
15
17
 
18
+ NO_KNOWLEDGE_MESSAGE_INTEL = DefaultMessageIntel(
19
+ text='Sorry, I could not find any information on that.'
20
+ )
21
+
16
22
 
17
23
  def knowledge_search_workflow(
18
24
  request: WSGIRequest,
19
25
  user_input: str,
20
26
  message_history: MessageHistory,
21
- ) -> KnowledgeMessageIntel | None:
27
+ ) -> BaseMessageIntel | None:
22
28
  collection_decoder = get_collection_decoder()
23
29
  collections = collection_decoder.process(user_input).values
24
30
 
25
31
  if collections[0] is None:
26
- return None
32
+ return NO_KNOWLEDGE_MESSAGE_INTEL
27
33
 
28
34
  entries = []
29
35
 
@@ -36,7 +42,7 @@ def knowledge_search_workflow(
36
42
  entries = [entry for entry in entries if entry is not None]
37
43
 
38
44
  if not entries:
39
- return None
45
+ return NO_KNOWLEDGE_MESSAGE_INTEL
40
46
 
41
47
  entries_intel = EntriesIntel(entry_intel_list=[])
42
48
 
@@ -0,0 +1,24 @@
1
+ # Generated by Django 5.2.7 on 2025-11-12 16:48
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_spire_core', '0001_initial'),
10
+ ('django_spire_knowledge', '0007_alter_collection_options'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='collection',
16
+ name='tags',
17
+ field=models.ManyToManyField(blank=True, editable=False, null=True, related_name='+', to='django_spire_core.tag'),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='entry',
21
+ name='tags',
22
+ field=models.ManyToManyField(blank=True, editable=False, null=True, related_name='+', to='django_spire_core.tag'),
23
+ ),
24
+ ]
@@ -46,4 +46,20 @@
46
46
  Last Edits ETC
47
47
  </div>
48
48
  </div>
49
+ <div class="row">
50
+ <div class="col-12 col-lg-8 mx-auto pb-4">
51
+ Tags:
52
+ {% for tag in collection.tags.all %}
53
+ <span class="badge bg-secondary">{{ tag.name }}</span>
54
+ {% endfor %}
55
+ </div>
56
+ </div>
57
+ <div class="row">
58
+ <div class="col-12 col-lg-8 mx-auto pb-4">
59
+ Aggregated Tags:
60
+ {% for tag in collection.services.tag.get_aggregated_tag_set %}
61
+ <span class="badge bg-secondary">{{ tag }}</span>
62
+ {% endfor %}
63
+ </div>
64
+ </div>
49
65
  {% endblock %}
@@ -21,6 +21,12 @@
21
21
  KNOWLEDGE_ENTRY_VERSION_EDITOR?.readOnly?.toggle()
22
22
  this.editor_is_readonly = KNOWLEDGE_ENTRY_VERSION_EDITOR?.readOnly?.isEnabled
23
23
 
24
+ if (this.editor_is_readonly) {
25
+ django_glue_fetch(
26
+ '{% url "django_spire:knowledge:entry:version:json:update_entry_from_version" pk=entry.id %}',
27
+ )
28
+ }
29
+
24
30
  let view_mode = this.editor_is_readonly ? 'readonly' : 'edit'
25
31
 
26
32
  let url = new URL(window.location)
@@ -16,32 +16,47 @@
16
16
  {% endblock %}
17
17
 
18
18
  {% block full_page_info_navigation %}
19
- <div>
20
- <div class="mb-3">
19
+ <div class="row">
20
+ <div class="col-12 mb-3">
21
21
  {% include 'django_spire/element/attribute_element.html' with attribute_title='Author' attribute_value=entry.current_version.author.get_full_name %}
22
22
  </div>
23
+ </div>
23
24
 
24
- <div class="mb-3">
25
+ <div class="row">
26
+ <div class="col-12 mb-3">
25
27
  {% include 'django_spire/element/attribute_element.html' with attribute_title='Status' %}
26
28
  {% include 'django_spire/knowledge/entry/badge/entry_status_badge.html' with status=entry.current_version.status %}
27
29
  </div>
30
+ </div>
28
31
 
29
- {% if entry.current_version.published_datetime %}
30
- <div class="mb-3">
32
+ {% if entry.current_version.published_datetime %}
33
+ <div class="row">
34
+ <div class="col-12 mb-3">
31
35
  {% include 'django_spire/element/attribute_element.html' with attribute_title='Published' attribute_value=entry.current_version.published_datetime %}
32
36
  </div>
33
- {% endif %}
37
+ </div>
38
+ {% endif %}
34
39
 
35
- <div class="mb-3">
40
+ <div class="row">
41
+ <div class="col-12 mb-3">
36
42
  {% include 'django_spire/element/attribute_element.html' with attribute_title='Last Edit' attribute_value=entry.current_version.last_edit_datetime %}
37
43
  </div>
38
44
  </div>
45
+
46
+ <div class="row">
47
+ <div class="col-12 mb-3">
48
+ {% include 'django_spire/element/attribute_element.html' with attribute_title='Tags' %}
49
+ {% for tag in entry.tags.all %}
50
+ <span class="badge bg-secondary">{{ tag.name }}</span>
51
+ {% endfor %}
52
+ </div>
53
+ </div>
39
54
  {% endblock %}
40
55
 
41
56
  {% block knowledge_full_page_content %}
42
57
  <div class="row justify-content-center ps-3">
43
58
  <div class="col-12">
44
- {% include 'django_spire/knowledge/entry/version/container/detail_container.html' %}
59
+ {% include 'django_spire/knowledge/entry/version/container/editor_container.html' %}
45
60
  </div>
46
61
  </div>
47
62
  {% endblock %}
@@ -49,6 +64,8 @@
49
64
  {% block base_body_additional_bottom_js %}
50
65
  {{ block.super }}
51
66
 
52
- <script src="{% static 'django_spire/knowledge/entry/version/js/null_paragraph.js' %}?v={{ DJANGO_SPIRE_VERSION }}" type="application/javascript"></script>
53
- <script src="{% static 'django_spire/knowledge/entry/version/js/editor.js' %}?v={{ DJANGO_SPIRE_VERSION }}" type="application/javascript"></script>
67
+ <script src="{% static 'django_spire/knowledge/entry/version/js/null_paragraph.js' %}?v={{ DJANGO_SPIRE_VERSION }}"
68
+ type="application/javascript"></script>
69
+ <script src="{% static 'django_spire/knowledge/entry/version/js/editor.js' %}?v={{ DJANGO_SPIRE_VERSION }}"
70
+ type="application/javascript"></script>
54
71
  {% endblock %}
django_spire/settings.py CHANGED
@@ -6,6 +6,7 @@ DJANGO_SPIRE_AUTH_CONTROLLERS = {
6
6
 
7
7
  # AI Settings
8
8
  AI_PERSONA_NAME = 'AI Assistant'
9
+ AI_CHAT_CALLABLE = None
9
10
  AI_CHAT_DEFAULT_CALLABLE = None
10
11
  AI_SMS_CONVERSATION_DEFAULT_CALLABLE = None
11
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-spire
3
- Version: 0.19.5
3
+ Version: 0.20.1
4
4
  Summary: A project for Django Spire
5
5
  Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
6
6
  License: Copyright (c) 2024 Stratus Advanced Technologies and Contributors.
@@ -49,7 +49,7 @@ Requires-Dist: beautifulsoup4>=4.14.2
49
49
  Requires-Dist: boto3>=1.34.0
50
50
  Requires-Dist: botocore>=1.34.0
51
51
  Requires-Dist: crispy-bootstrap5==2024.10
52
- Requires-Dist: dandy>=1.3.1
52
+ Requires-Dist: dandy>=1.3.3
53
53
  Requires-Dist: django>=5.1.8
54
54
  Requires-Dist: django-crispy-forms==2.3
55
55
  Requires-Dist: django-glue>=0.8.1
@@ -1,8 +1,8 @@
1
1
  django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  django_spire/conf.py,sha256=c5Hs-7lk9T15254tOasiQ2ZTFLQIVJof9_QJDfm1PAI,933
3
- django_spire/consts.py,sha256=SU73G0g9_xGmc_PGmEkqx21_G17v-uZ6ZIFBRnr0nZQ,171
3
+ django_spire/consts.py,sha256=0pi9-zQb-ooeNNpnBBaPK6lSS7BO1hC2KSZHRlgbhcE,171
4
4
  django_spire/exceptions.py,sha256=L5ndRO5ftMmh0pHkO2z_NG3LSGZviJ-dDHNT73SzTNw,48
5
- django_spire/settings.py,sha256=_bM5uUqJXq3sW-NZBNAzt4egZwmvLq_jA8DaQGHqVoE,661
5
+ django_spire/settings.py,sha256=5gzQCMKAHcBYFREPrYLkQcqFj5gSSgyduUU1H2ztWN8,685
6
6
  django_spire/urls.py,sha256=mKeZszb5U4iIGqddMb5Tt5fRC72U2wABEOi6mvOfEBU,656
7
7
  django_spire/utils.py,sha256=kW0HP1xWj8Oz0h1GWs4NflnD8Jq8_F4hABwKTiT-Iyk,1006
8
8
  django_spire/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -25,9 +25,9 @@ django_spire/ai/chat/auth/controller.py,sha256=l4xHQxSlPgS_QPvofsARUOMcXzVo3cTMs
25
25
  django_spire/ai/chat/intelligence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  django_spire/ai/chat/intelligence/prompts.py,sha256=jnFaN7EUUQM-wxloWJZ2UAGw1RDXIOKkX4JfvT6ukxw,686
27
27
  django_spire/ai/chat/intelligence/decoders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- django_spire/ai/chat/intelligence/decoders/tools.py,sha256=8fSnuWL1Apr32-hH52Fz9UUAA0Ph54EYUgohAXnwGIM,933
28
+ django_spire/ai/chat/intelligence/decoders/tools.py,sha256=pC3Rvg0a1bgi0ESAsDH2HDJQTsu9aCcTslXfp4R7HwQ,1233
29
29
  django_spire/ai/chat/intelligence/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- django_spire/ai/chat/intelligence/workflows/chat_workflow.py,sha256=a5rMWbSEBHFe_EAiWpiow0WanAdibLdywtJfrL0oBX8,2289
30
+ django_spire/ai/chat/intelligence/workflows/chat_workflow.py,sha256=VaaIqN2U5C1opyQXyJevDs9fDtfwn7gVm5-EZDju1EI,2475
31
31
  django_spire/ai/chat/migrations/0001_initial.py,sha256=1cbREhX3_fNsbfumJoKAZ8w91Kq5NeXUn_iI45B7oGE,2632
32
32
  django_spire/ai/chat/migrations/0002_remove_chatmessage_content_chatmessage__content_and_more.py,sha256=KeNT7VRFmwA74odh9wxIE1Cr4KAO4Tmtqu0FuI2AmK0,752
33
33
  django_spire/ai/chat/migrations/0003_rename__content_chatmessage__intel_data_and_more.py,sha256=wAcJ6Ia3fWrGbqnVrqD2C3-3ijAot0LK-B3KZavoY_A,754
@@ -494,6 +494,8 @@ django_spire/core/management/commands/spire_startapp_pkg/template/templates/page
494
494
  django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${list_page_template_name}.html.template,sha256=Ve0RVvWjMU6m33wXdDv10MkDoEFKWULuu-1_BK-O4aI,254
495
495
  django_spire/core/middleware/__init__.py,sha256=BvBwVHLVebknJkObtKk_s5OqaIJaAmnavTvnnv4TBO8,149
496
496
  django_spire/core/middleware/maintenance.py,sha256=jJtmz69UXJBBgPkq5SNspWuER1bGfAjSnsVipYS4TF0,921
497
+ django_spire/core/migrations/0001_initial.py,sha256=kbwHzfNnEf10O1rvqjXPhTyMT0ThZll_YtxHxDpq9GY,675
498
+ django_spire/core/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
497
499
  django_spire/core/redirect/__init__.py,sha256=ubAsz_h3ufwgNNkdtqiJQDgXc6DodE2LTUyBwi9dgkE,71
498
500
  django_spire/core/redirect/generic_redirect.py,sha256=bfpA2GSRbkf5Y_zqTiTGzakQauLumm3JbaYMzmsDhjA,1245
499
501
  django_spire/core/redirect/safe_redirect.py,sha256=deGLqiR1wWwqlJ8BYp76qDUDHnfRrxL-1Vns3nozSG0,2901
@@ -572,6 +574,16 @@ django_spire/core/static/django_spire/js/modal.js,sha256=ay0Sovi0HhedEz3dRhB_INh
572
574
  django_spire/core/static/django_spire/js/session_controller.js,sha256=aMom087y00MUhdxrZlrg89f88mBC7cWEhGWopeb5hlQ,232
573
575
  django_spire/core/static/django_spire/js/theme.js,sha256=APZnTjuhgXV7m1vVlmvNIMZACJ0gG8uGeiyWSmhZTmQ,8512
574
576
  django_spire/core/static/django_spire/js/ui.js,sha256=Xrnw6cMmddCiUKC7iAJn1U7lk3nMJYxFJJO-J5zCqSw,673
577
+ django_spire/core/tags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
578
+ django_spire/core/tags/mixins.py,sha256=ibnPcLmdB0js8ha4M93CqRv5kOQ7fijKBCLoxM9F7C0,1979
579
+ django_spire/core/tags/models.py,sha256=zKXITkpyYNe80-CmJh46qw0YqxbJM79ud2wpgznmZH4,819
580
+ django_spire/core/tags/querysets.py,sha256=vLdAV9KRTn-B9G9vhB4tQ3j-iNyz6OVZR5E9nVvAZY8,263
581
+ django_spire/core/tags/tools.py,sha256=CB8vOlFSmk8Q79x-5OGYXCGx-VteTgDAPUCVOYaTmhU,787
582
+ django_spire/core/tags/intelligence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
583
+ django_spire/core/tags/intelligence/tag_set_bot.py,sha256=-U-dkyodua48F6xatV1XHzeTTO-Zukl6rYOLMLxdIig,1289
584
+ django_spire/core/tags/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
585
+ django_spire/core/tags/tests/test_intelligence.py,sha256=UJj9OL_RaZQZ0tVih1VjntB9K5UkTg__VwE7iOr3Kog,1074
586
+ django_spire/core/tags/tests/test_tags.py,sha256=dTf0hkbgmnGK4HjdKO21dOdZLuZ2qqgeatsQvqqNeEg,3516
575
587
  django_spire/core/templates/django_spire/403.html,sha256=35OX-3z-Yi6Igx9DTAcxNwWiZ17wXdm4wYGML4U2pns,37
576
588
  django_spire/core/templates/django_spire/404.html,sha256=91sTr518M2YxQ8X3GlzDsNP6IVo3MW07tbHdcLBL8Iw,235
577
589
  django_spire/core/templates/django_spire/500.html,sha256=8N-aqCoafjq2mFcjLZv-BkAEqRh2bzJ2SqojlboS8g4,247
@@ -796,9 +808,9 @@ django_spire/knowledge/models.py,sha256=9NtBiv6HD6dcfL9AGMf5n6Z14hKciDkQ7Fu2zzOB
796
808
  django_spire/knowledge/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
797
809
  django_spire/knowledge/auth/controller.py,sha256=ktTJ7gjtbYmCYESZnK97q7_fPojTAScZXRysg2F_86Y,929
798
810
  django_spire/knowledge/collection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
799
- django_spire/knowledge/collection/admin.py,sha256=YI28TqSxzmYOLCVeRsOrrzB-YNuOdQapQRHbSO35hHA,714
811
+ django_spire/knowledge/collection/admin.py,sha256=9OKEgiSDiSHX6ZwB11l6mCx-jdF4VKfOe-SwFBdjKK4,1370
800
812
  django_spire/knowledge/collection/forms.py,sha256=7JovVxNzE67hSydDa-1JGK4U4zPyzOj605QyEl0JkP8,238
801
- django_spire/knowledge/collection/models.py,sha256=BauMvNfRNCGP1RjS3Yg1ERSIYjnaMixEBewpGF4eDAE,2302
813
+ django_spire/knowledge/collection/models.py,sha256=_wqLIzOHwvsVxdc-GjEZeXWKo_MOHF7DyaHCEPuaCwM,2390
802
814
  django_spire/knowledge/collection/querysets.py,sha256=8DWTGvo4WovXQ9n7XRLoxtyDdBUka2N-kcPUnJVOhOo,2479
803
815
  django_spire/knowledge/collection/seeding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
804
816
  django_spire/knowledge/collection/seeding/seed.py,sha256=i4VEvScJvZ1SUV9ukKZp0qdh_owyLyVSHGgl8vIzhwQ,303
@@ -807,7 +819,8 @@ django_spire/knowledge/collection/services/__init__.py,sha256=47DEQpj8HBSa-_TImW
807
819
  django_spire/knowledge/collection/services/factory_service.py,sha256=92DLZODSRPF-iA9r3Jgys2GzbC39KssGEk9eOJ5wZjs,1298
808
820
  django_spire/knowledge/collection/services/ordering_service.py,sha256=vTkSdt1lVT4VxSK3GhZibj8FPDDy5butwPkVzqmm07g,1243
809
821
  django_spire/knowledge/collection/services/processor_service.py,sha256=3ArKBndoL3HEvH_1EBqfiw7AnbFYZ0WmBgcPV8BHdLQ,911
810
- django_spire/knowledge/collection/services/service.py,sha256=Vg9ZPWlCxZ_43fkudvBUBZfd7L7A8_vKlBA9dIHUNas,1709
822
+ django_spire/knowledge/collection/services/service.py,sha256=sEamdqRcKdNcL6yz18hok0jww3Z4VRCPWrMF2QLvKGE,1830
823
+ django_spire/knowledge/collection/services/tag_service.py,sha256=7EUIoTn70oAvcsX53RvKMLTZ_sySmBNgWhBvg8RCesE,1430
811
824
  django_spire/knowledge/collection/services/transformation_service.py,sha256=ME_jYURSG7at4oujTbcRzIVXyKnb75Xi6M4Nclbw6Yo,3464
812
825
  django_spire/knowledge/collection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
813
826
  django_spire/knowledge/collection/tests/factories.py,sha256=NUu4OliEZIQL9c05RPIPdzYP3Dtn1szLZxP4Wl-kX2w,429
@@ -822,13 +835,13 @@ django_spire/knowledge/collection/urls/form_urls.py,sha256=yHU3sbaA3w-m9rVvon6nX
822
835
  django_spire/knowledge/collection/urls/json_urls.py,sha256=XZp5wt8m9x38Uuy1dhegqZ4qlJLZK2EgJsLltx97o0Q,195
823
836
  django_spire/knowledge/collection/urls/page_urls.py,sha256=M8zZJJzVmMx7QUc3uGIrjyb8-Kz00fz7u0mHYqFZf3M,280
824
837
  django_spire/knowledge/collection/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
825
- django_spire/knowledge/collection/views/form_views.py,sha256=qEbQj0jEO1jKYBHVNQkoymI-PtoHIOUCtddzpUOptpw,2954
838
+ django_spire/knowledge/collection/views/form_views.py,sha256=ziQhlrLEJ62VzYiIAApd0ZThM2jc-who-qw0_BlO-VI,3014
826
839
  django_spire/knowledge/collection/views/json_views.py,sha256=uzSD83O7p-L5Oh8jo_Ejh-4lbo5zXjLkZGe0cn6SXSw,1030
827
840
  django_spire/knowledge/collection/views/page_views.py,sha256=jiKsIqi9pvSY5KbLLGDdTHSPHXmnd-7oyYXckF2ONh4,2045
828
841
  django_spire/knowledge/entry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
829
- django_spire/knowledge/entry/admin.py,sha256=icff-hpV6Uj-WszcWujHMljo6oKo4kft_Zlzcm0I6sQ,986
842
+ django_spire/knowledge/entry/admin.py,sha256=pY-0sYx24Su0y7sb1I2-GMCZxFEZj9oXhRckEdHNKno,1568
830
843
  django_spire/knowledge/entry/forms.py,sha256=Mhol37dMJP-pnex5IPxQVdchYcFBXnGzcWoY0SkU7lo,339
831
- django_spire/knowledge/entry/models.py,sha256=OhmzCBcV_XGDwmBgx3kfSHRZj9ztLj3Kri3c5QDw4PM,2054
844
+ django_spire/knowledge/entry/models.py,sha256=RSWXFLWAPZ43klgZvqXEzYl39g4hNIUPmX7btOrzbGk,2141
832
845
  django_spire/knowledge/entry/querysets.py,sha256=oVTylFT_Zx0elT5IVbYZRa0FwyGd9YaRz0vdcumfrsU,1121
833
846
  django_spire/knowledge/entry/seeding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
834
847
  django_spire/knowledge/entry/seeding/seed.py,sha256=n7ereq118GuqV1luNih7nuVpkrZpkBgPWt0FuE4Bl88,105
@@ -837,7 +850,8 @@ django_spire/knowledge/entry/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
837
850
  django_spire/knowledge/entry/services/automation_service.py,sha256=0_Ypk5cXhGvkQLkmKBArFTrE_sZhXCnBencmuJeLxMI,1826
838
851
  django_spire/knowledge/entry/services/factory_service.py,sha256=bgUEHjdc9u2mxUJFy26qYOxUiJRHmhtcoH6AG3xzU0I,1379
839
852
  django_spire/knowledge/entry/services/processor_service.py,sha256=sfAM5ku7Td4k0jCe2szyoR72Bt1_OS75UKh7YPL4l2s,494
840
- django_spire/knowledge/entry/services/service.py,sha256=n8N2pshbE977bNoN5zf0gO-Kyy9LQLcGOJ4MMNT8Uj8,1806
853
+ django_spire/knowledge/entry/services/service.py,sha256=-A8_UcmQ8aLvkRNzGzTMA4faN9UWOd5pdlFlujB1YHI,1912
854
+ django_spire/knowledge/entry/services/tag_service.py,sha256=AA4xDmofhuFOxa6HuKqlM9_SY-kjPWLKAnPUhvA7Is4,793
841
855
  django_spire/knowledge/entry/services/tool_service.py,sha256=QL6w8t284LfSOJrQaF46uWInd4t413WYxcXCWsat_jE,1083
842
856
  django_spire/knowledge/entry/services/transformation_services.py,sha256=6WPQxW-_XATi8HCwshtJjoYbQFJcIdCqFaADHnTyk1U,2687
843
857
  django_spire/knowledge/entry/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -903,11 +917,11 @@ django_spire/knowledge/entry/version/tests/test_urls/__init__.py,sha256=47DEQpj8
903
917
  django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py,sha256=WFN-aR6NmZGNKv03Jlw10YR730JYpbydYHDQPPbbkJI,1161
904
918
  django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py,sha256=l3k7lXeRNbbtIXZtt8LLDacDozYjTzinSCAjJdPeGC4,946
905
919
  django_spire/knowledge/entry/version/urls/__init__.py,sha256=e0MeLIxK5W4H68nK3A5TKbD7ZJFwFFJ2bZNhh2T-4uk,394
906
- django_spire/knowledge/entry/version/urls/json_urls.py,sha256=Mns6ZTB5-52iHbaFw5Cb5elw-PqhYa3mxBTeSNY2saw,258
920
+ django_spire/knowledge/entry/version/urls/json_urls.py,sha256=w1QWnVGHHIrLgXJYoDBJB0q07WCZzQTBeBotLtXQjoE,414
907
921
  django_spire/knowledge/entry/version/urls/page_urls.py,sha256=z8pfK548b4xr6UIE3pnPNTUjSeZaeBac2MQeL4VyJTo,204
908
922
  django_spire/knowledge/entry/version/urls/redirect_urls.py,sha256=9X7UqKYIxwDRVPHTwT1zywpWybD1hJ923p3cX1itZ5Q,219
909
923
  django_spire/knowledge/entry/version/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
910
- django_spire/knowledge/entry/version/views/json_views.py,sha256=9QyqFHrYS-BlfIDUfc-IrOu3SJKBbSSQw7jRxo51p0U,835
924
+ django_spire/knowledge/entry/version/views/json_views.py,sha256=gauZ4YvUks7GNWR212aaPKgE6ym5YDKVLChKWGfqljs,1180
911
925
  django_spire/knowledge/entry/version/views/page_views.py,sha256=rRoaqlChmcnWVLO02smw_zIHrec-3MtQ-FcDGzOQZh0,1698
912
926
  django_spire/knowledge/entry/version/views/redirect_views.py,sha256=m6RPYfBcnOq-iU2QND56IfCHU7wCqF9aOiQUCbqP_Aw,754
913
927
  django_spire/knowledge/entry/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -919,14 +933,14 @@ django_spire/knowledge/intelligence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
919
933
  django_spire/knowledge/intelligence/bots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
920
934
  django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py,sha256=02fRELEyRjqRc1o_McQPWucQOTQtpIO6_v8-3OO7mLs,1689
921
935
  django_spire/knowledge/intelligence/decoders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
922
- django_spire/knowledge/intelligence/decoders/collection_decoder.py,sha256=v9HvNX307bu-vpOMHjOanu6RhtV_SsAUmkHqWu4F0II,583
923
- django_spire/knowledge/intelligence/decoders/entry_decoder.py,sha256=n_BZyudiki-SrwIXSZvnSF1aajrARD59oZMqC__0Jo8,503
936
+ django_spire/knowledge/intelligence/decoders/collection_decoder.py,sha256=Vqh-JQukPatbxvV3NMWKvl8Np3bgRtDxQZjd2T3E0Rc,599
937
+ django_spire/knowledge/intelligence/decoders/entry_decoder.py,sha256=q6RzbCz0Ph00wJ9p8Ujp-2hYwppeRZa8Pj_-0YDwYlI,516
924
938
  django_spire/knowledge/intelligence/intel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
925
939
  django_spire/knowledge/intelligence/intel/collection_intel.py,sha256=4ci6_bVUimj_33FZgQ7wCpESXrKBMxHGF0yyfrstmvI,144
926
940
  django_spire/knowledge/intelligence/intel/entry_intel.py,sha256=lGlGqeruV5oOu0aArGA_Bxa1pqAI7lr1MD7UATAy2Sg,445
927
941
  django_spire/knowledge/intelligence/intel/message_intel.py,sha256=yLTlFZ3dRJU800yOtp3Tme3TZdmi8pf02gbo6uZlUps,420
928
942
  django_spire/knowledge/intelligence/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
929
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py,sha256=ROytkx1qlpq-oVj1UrVn9OyZ_5nXBUc6Pjj-s1qffxQ,1656
943
+ django_spire/knowledge/intelligence/workflows/knowledge_workflow.py,sha256=s_KXlpYSwG89slEItbEA9pEPiH8DyAmRQ-VpZEP36Ns,1894
930
944
  django_spire/knowledge/migrations/0001_initial.py,sha256=0oAx4e0Tu1wbcMwQZ4I_B2X_EGdEA9Qz_UBdVNGc3yE,5487
931
945
  django_spire/knowledge/migrations/0002_alter_entryversionblock_type.py,sha256=Eg8tLjDO0nJ3go_jgVvoqwYKFOpYUUyNtZtLI2V3IQQ,527
932
946
  django_spire/knowledge/migrations/0003_alter_collection_order_alter_entry_order_and_more.py,sha256=nIIUg1lHQ1iLbPlOulwZgzHrP1erGMYVXDaEdDXv4ik,749
@@ -934,6 +948,7 @@ django_spire/knowledge/migrations/0004_alter_collection_options_collectiongroup.
934
948
  django_spire/knowledge/migrations/0005_entryversionblock__tunes_data_and_more.py,sha256=sA_YCnFX6R2Wt9OecVpW4Sj27k2iRcw4KbyJVdcQYqY,771
935
949
  django_spire/knowledge/migrations/0006_alter_entryversionblock_type.py,sha256=-FPS118i3wwbfuXIPv4fNQiKu59X35E2r2zEFgDyNgo,516
936
950
  django_spire/knowledge/migrations/0007_alter_collection_options.py,sha256=jUqOHnY9T7QjguVhNC2uYVJzlWqOwWeSVe26ElySkY0,587
951
+ django_spire/knowledge/migrations/0008_collection_tags_entry_tags.py,sha256=QnCm7ULhPfBUJ8Ut7CKv4sG41S4gRkZX06jcWKBvB_M,754
937
952
  django_spire/knowledge/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
938
953
  django_spire/knowledge/seeding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
939
954
  django_spire/knowledge/seeding/seed.py,sha256=LRhVfPHiFluSLLtjSY44YM9Oo1Hvfs_U-ksRvKk7pOg,97
@@ -950,7 +965,7 @@ django_spire/knowledge/templates/django_spire/knowledge/collection/container/det
950
965
  django_spire/knowledge/templates/django_spire/knowledge/collection/element/ellipsis_dropdown.html,sha256=o9cceexfwUSDtac4uhlUm4LimnBQrgTP6QPkpJF6DD0,1376
951
966
  django_spire/knowledge/templates/django_spire/knowledge/collection/form/form.html,sha256=gDjvFBpNQHGn4PNJBi4pBw89-IakpyrC23D9dr0xQ-c,1902
952
967
  django_spire/knowledge/templates/django_spire/knowledge/collection/item/collection_item.html,sha256=gaOqh1wxf5IcWdxWWiYyz6HWjoPyn6FohaTMIssJ67k,517
953
- django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html,sha256=4RLjbrfyjDad6z-kJo2tJZLXMKxU52bkKihiEZApKqg,1724
968
+ django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html,sha256=v6lLzrubEYzVgoTKt_Oh3xAV0agUkWmyTFtAmD5uQcg,2281
954
969
  django_spire/knowledge/templates/django_spire/knowledge/collection/page/form_page.html,sha256=eD190iYnmx-6u8Y0EAExDdilPtxTlS_ic6-WBfTtj2s,274
955
970
  django_spire/knowledge/templates/django_spire/knowledge/container/container.html,sha256=Fi-PL8neKrmGeLQtvhNiGoimpen86m4r9YlAS-amWXI,1220
956
971
  django_spire/knowledge/templates/django_spire/knowledge/entry/badge/entry_status_badge.html,sha256=qz7Y2ac1T4qu0Nr_Yj4BdUbQamgA6Llq7TmoXAf5Arw,381
@@ -966,8 +981,8 @@ django_spire/knowledge/templates/django_spire/knowledge/entry/item/list_item.htm
966
981
  django_spire/knowledge/templates/django_spire/knowledge/entry/modal/publish_confirm_modal.html,sha256=7SsTjvk_iOdBgnYy83pMBPIdbuvJadd4h5XI5mPos5o,724
967
982
  django_spire/knowledge/templates/django_spire/knowledge/entry/page/form_page.html,sha256=EBfDjDBvo7IR6f5joTtZjzK5LtZb4rt0U1FO1HQNt88,291
968
983
  django_spire/knowledge/templates/django_spire/knowledge/entry/page/import_form_page.html,sha256=TK8Bc7mUZwsmV1Xm9FPyWz7Hws68Ijy4c55orvNoOC4,308
969
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/detail_container.html,sha256=UPCGcjKm7CKQC6A7wFCG2U80BGo_v5JIhnCCTIUP_3c,3123
970
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html,sha256=yzbZVhBa_xvAqloWBrraB4EqPufQMbhJ9HtyL9MqLqo,2536
984
+ django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/editor_container.html,sha256=NAtXEq8UJOV9_vp3l5HIIdQp-lZhNVW4luF0Nvv8Sl8,3389
985
+ django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html,sha256=Q6bPXeIORGQMZ4Wb6YDsWWoMUKws6a-oGhl4mv7syUE,3019
971
986
  django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/form_page.html,sha256=ufUISOfNmk45hl7F-ujtAzpHdEmUxh55odW0eyL6SMU,722
972
987
  django_spire/knowledge/templates/django_spire/knowledge/message/knowledge_message_intel.html,sha256=fpCJAwgxYqzJPLV63mLmUFjfEFk98t4FBX0RkSvDnzQ,698
973
988
  django_spire/knowledge/templates/django_spire/knowledge/page/full_page.html,sha256=vz3WQ6SAL43QALRf0haaLfNkUfIQQemR0FAZQ2tznV4,1331
@@ -1127,8 +1142,8 @@ django_spire/theme/urls/page_urls.py,sha256=S8nkKkgbhG3XHI3uMUL-piOjXIrRkuY2UlM_
1127
1142
  django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1128
1143
  django_spire/theme/views/json_views.py,sha256=W1khC2K_EMbEzAFmMxC_P76_MFnkRH4-eVdodrRfAhw,1904
1129
1144
  django_spire/theme/views/page_views.py,sha256=pHr8iekjtR99xs7w1taj35HEo133Svq1dvDD0y0VL1c,3933
1130
- django_spire-0.19.5.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
1131
- django_spire-0.19.5.dist-info/METADATA,sha256=jzU9HqJVRDWzw6SjQAToWERibaqyr-NGGS7Njq0Xo-c,4967
1132
- django_spire-0.19.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1133
- django_spire-0.19.5.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1134
- django_spire-0.19.5.dist-info/RECORD,,
1145
+ django_spire-0.20.1.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
1146
+ django_spire-0.20.1.dist-info/METADATA,sha256=2DlaKuP0TZp6LeY4ytPWI7ckgoQS_4lF5GonhxR-E6A,4967
1147
+ django_spire-0.20.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1148
+ django_spire-0.20.1.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1149
+ django_spire-0.20.1.dist-info/RECORD,,