django-spire 0.21.1__py3-none-any.whl → 0.22.0__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 (40) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/core/{tags → tag}/intelligence/tag_set_bot.py +5 -4
  3. django_spire/core/tag/mixins.py +23 -0
  4. django_spire/core/{tags → tag}/models.py +1 -1
  5. django_spire/core/tag/service/tag_service.py +72 -0
  6. django_spire/core/{tags → tag}/tests/test_intelligence.py +1 -1
  7. django_spire/core/tag/tests/test_tags.py +102 -0
  8. django_spire/core/tag/tools.py +66 -0
  9. django_spire/core/templates/django_spire/tag/element/tag.html +1 -0
  10. django_spire/core/templatetags/spire_core_tags.py +12 -0
  11. django_spire/knowledge/collection/models.py +2 -2
  12. django_spire/knowledge/collection/services/tag_service.py +16 -14
  13. django_spire/knowledge/entry/models.py +2 -2
  14. django_spire/knowledge/entry/querysets.py +5 -0
  15. django_spire/knowledge/entry/services/tag_service.py +5 -5
  16. django_spire/knowledge/intelligence/bots/entries_search_llm_bot.py +44 -0
  17. django_spire/knowledge/intelligence/intel/entry_intel.py +24 -4
  18. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +46 -25
  19. django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +6 -4
  20. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +3 -2
  21. django_spire/knowledge/templates/django_spire/knowledge/message/knowledge_message_intel.html +14 -6
  22. django_spire/settings.py +1 -1
  23. {django_spire-0.21.1.dist-info → django_spire-0.22.0.dist-info}/METADATA +2 -2
  24. {django_spire-0.21.1.dist-info → django_spire-0.22.0.dist-info}/RECORD +32 -34
  25. django_spire/core/tags/mixins.py +0 -61
  26. django_spire/core/tags/tests/test_tags.py +0 -102
  27. django_spire/core/tags/tools.py +0 -20
  28. django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +0 -45
  29. django_spire/knowledge/intelligence/decoders/collection_decoder.py +0 -19
  30. django_spire/knowledge/intelligence/decoders/entry_decoder.py +0 -22
  31. django_spire/knowledge/intelligence/intel/collection_intel.py +0 -8
  32. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/form_page.html +0 -26
  33. /django_spire/core/{tags → tag}/__init__.py +0 -0
  34. /django_spire/core/{tags → tag}/intelligence/__init__.py +0 -0
  35. /django_spire/core/{tags → tag}/querysets.py +0 -0
  36. /django_spire/core/{tags/tests → tag/service}/__init__.py +0 -0
  37. /django_spire/{knowledge/intelligence/decoders → core/tag/tests}/__init__.py +0 -0
  38. {django_spire-0.21.1.dist-info → django_spire-0.22.0.dist-info}/WHEEL +0 -0
  39. {django_spire-0.21.1.dist-info → django_spire-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  40. {django_spire-0.21.1.dist-info → django_spire-0.22.0.dist-info}/top_level.txt +0 -0
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.21.1'
1
+ __VERSION__ = '0.22.0'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -13,10 +13,11 @@ class TagSetBot(Bot):
13
13
  Prompt()
14
14
  .list([
15
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.',
16
+ 'Include tags that help searchability.',
17
+ 'Focus on tagging the words in the content.',
18
+ 'Only add additional words that are very relevant to the content.',
19
+ 'Use spaces to separate words in tags.',
20
+ 'Include common acronyms in addition to the tags.',
20
21
  ])
21
22
  )
22
23
 
@@ -0,0 +1,23 @@
1
+ from django.db import models
2
+
3
+ from django_spire.core.tag.models import Tag
4
+ from django_spire.core.tag.tools import simplify_and_weight_tag_set_to_dict, simplify_tag_set
5
+
6
+
7
+ class TagModelMixin(models.Model):
8
+ tags = models.ManyToManyField(Tag, related_name='+', null=True, blank=True, editable=False)
9
+
10
+ class Meta:
11
+ abstract = True
12
+
13
+ @property
14
+ def tag_set(self) -> set[str]:
15
+ return set(self.tags.values_list('name', flat=True))
16
+
17
+ @property
18
+ def simplified_tag_set(self) -> set[str]:
19
+ return simplify_tag_set(self.tag_set)
20
+
21
+ @property
22
+ def simplified_and_weighted_tag_dict(self) -> dict[str, int]:
23
+ return simplify_and_weight_tag_set_to_dict(self.tag_set)
@@ -3,7 +3,7 @@ from typing import Sequence, Self
3
3
  from django.db import models
4
4
  from django.utils.text import slugify
5
5
 
6
- from django_spire.core.tags.querysets import TagQuerySet
6
+ from django_spire.core.tag.querysets import TagQuerySet
7
7
 
8
8
 
9
9
  class Tag(models.Model):
@@ -0,0 +1,72 @@
1
+ from abc import abstractmethod, ABC
2
+ from typing import Generic
3
+
4
+ from django.db.models import QuerySet
5
+
6
+ from django_spire.contrib.constructor.django_model_constructor import TypeDjangoModel
7
+ from django_spire.contrib.service import BaseDjangoModelService
8
+ from django_spire.core.tag import tools
9
+ from django_spire.core.tag.models import Tag
10
+ from django_spire.core.tag.tools import get_score_percentage_from_tag_set_weighted
11
+
12
+
13
+ class BaseTagService(
14
+ BaseDjangoModelService[TypeDjangoModel],
15
+ ABC,
16
+ Generic[TypeDjangoModel]
17
+ ):
18
+ obj: TypeDjangoModel
19
+
20
+ @abstractmethod
21
+ def process_and_set_tags(self):
22
+ raise NotImplementedError
23
+
24
+ def add_tags_from_tag_set(self, tag_set: set[str]):
25
+ self._update_global_tags_from_set(tag_set)
26
+
27
+ self.obj.tags.add(*Tag.objects.in_tag_set(tag_set))
28
+
29
+ def clear_tags(self):
30
+ self.obj.tags.clear()
31
+
32
+ def get_matching_tags_from_tag_set(self, tag_set: set[str]) -> QuerySet:
33
+ return self.obj.tags.in_tag_set(tag_set)
34
+
35
+ def get_matching_percentage_of_tag_set(self, tag_set: set[str]) -> float:
36
+ return tools.get_matching_a_percentage_from_tag_sets(tag_set, self.obj.tag_set)
37
+
38
+ def get_matching_percentage_of_model_tags_from_tag_set(self, tag_set: set[str]) -> float:
39
+ return tools.get_matching_b_percentage_from_tag_sets(tag_set, self.obj.tag_set)
40
+
41
+ def get_score_percentage_from_tag_set_weighted(self, tag_set: set[str]) -> float:
42
+ return get_score_percentage_from_tag_set_weighted(
43
+ tag_set_actual=tag_set,
44
+ tag_set_reference=self.obj.tag_set
45
+ )
46
+
47
+ def get_simplified_and_weighted_tag_dict_above_minimum(self, minimum_weight: int = 1) -> dict[str, int]:
48
+ above_minimum_tag_dict = self.obj.simplified_and_weighted_tag_dict
49
+
50
+ return {
51
+ tag: weight
52
+ for tag, weight in above_minimum_tag_dict.items()
53
+ if weight >= minimum_weight
54
+ }
55
+
56
+ def has_tags_in_tag_set(self, tag_set: set[str]) -> bool:
57
+ return self.obj.tag_set.issuperset(tag_set)
58
+
59
+ def remove_tags_by_tag_set(self, tag_set: set[str]):
60
+ tag_objects = Tag.objects.in_tag_set(tag_set)
61
+
62
+ self.obj.tags.remove(*tag_objects)
63
+
64
+ def set_tags_from_tag_set(self, tag_set: set[str]):
65
+ self._update_global_tags_from_set(tag_set)
66
+
67
+ tag_objects = Tag.objects.in_tag_set(tag_set)
68
+ self.obj.tags.set(tag_objects)
69
+
70
+ @staticmethod
71
+ def _update_global_tags_from_set(tag_set: set[str]):
72
+ Tag.add_tags([Tag(name=tag) for tag in tag_set])
@@ -1,6 +1,6 @@
1
1
  from unittest import TestCase
2
2
 
3
- from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
3
+ from django_spire.core.tag.intelligence.tag_set_bot import TagSetBot
4
4
 
5
5
  TEST_INPUT = """
6
6
  I love reading books about science fiction, fantasy, and adventure, especially ones
@@ -0,0 +1,102 @@
1
+ from django.test import TestCase
2
+
3
+ from django_spire.core.tag.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.services.tag.add_tags_from_tag_set(self.tag_set_a)
22
+ self.collection.services.tag.add_tags_from_tag_set(self.tag_set_b)
23
+ self.collection.services.tag.add_tags_from_tag_set(self.tag_set_c)
24
+
25
+ self.collection.services.tag.add_tags_from_tag_set(self.mixed_tag_set)
26
+
27
+ self.collection.services.tag.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.services.tag.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.services.tag.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.services.tag.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.services.tag.remove_tags_by_tag_set(self.tag_set_b)
56
+
57
+ self.assertTrue(self.collection.services.tag.has_tags_in_tag_set(self.tag_set_c))
58
+ self.assertFalse(self.collection.services.tag.has_tags_in_tag_set(self.mixed_tag_set))
59
+
60
+ def test_tag_matching_methods(self):
61
+ self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)), 3)
62
+
63
+ self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_a)
64
+
65
+ self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)), 0)
66
+ self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)), 3)
67
+ self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.mixed_tag_set)), 2)
68
+
69
+ self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_b)
70
+
71
+ self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)), 0)
72
+
73
+ self.assertEqual(len(self.collection.services.tag.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.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a),
78
+ 1.00,
79
+ 2
80
+ )
81
+
82
+ self.assertAlmostEqual(
83
+ self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
84
+ 0.25,
85
+ 2
86
+ )
87
+
88
+ self.collection.services.tag.remove_tags_by_tag_set(self.mixed_tag_set)
89
+
90
+ self.assertAlmostEqual(
91
+ self.collection.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a),
92
+ 0.666,
93
+ 2
94
+ )
95
+
96
+
97
+ self.assertAlmostEqual(
98
+ self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
99
+ 0.222,
100
+ 2
101
+ )
102
+
@@ -0,0 +1,66 @@
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
21
+
22
+
23
+ def get_score_percentage_from_tag_set_weighted(tag_set_actual: set[str], tag_set_reference: set[str]) -> float:
24
+ total_points = len(simplify_tag_set(tag_set_reference))
25
+
26
+ multiplier = get_matching_count_from_tag_sets(tag_set_actual, tag_set_reference)
27
+
28
+ weighted_points = sum(
29
+ weight
30
+ for tag, weight in simplify_and_weight_tag_set_to_dict(tag_set_reference).items()
31
+ if tag in tag_set_actual
32
+ )
33
+
34
+ if weighted_points == 0 or total_points == 0:
35
+ return 0.0
36
+
37
+ return (weighted_points / total_points) * multiplier
38
+
39
+
40
+ def simplify_tag_set(tag_set: set[str]) -> set[str]:
41
+ return set(simplify_tag_set_to_list(tag_set))
42
+
43
+
44
+ def simplify_tag_set_to_list(tag_set: set[str]) -> list[str]:
45
+ simplified_tag_words = []
46
+
47
+ for tag in tag_set:
48
+ tag_words = tag.split('-')
49
+ simplified_tag_words.extend(tag_words)
50
+
51
+ return simplified_tag_words
52
+
53
+
54
+ def simplify_and_weight_tag_set_to_dict(tag_set: set[str]) -> dict[str, int]:
55
+ simplified_and_weighted_tag_words = {}
56
+
57
+ for tag_word in simplify_tag_set_to_list(tag_set):
58
+ simplified_and_weighted_tag_words[tag_word] = simplified_and_weighted_tag_words.get(tag_word, 0) + 1
59
+
60
+ return dict(
61
+ sorted(
62
+ simplified_and_weighted_tag_words.items(),
63
+ key=lambda item: item[1],
64
+ reverse=True
65
+ )
66
+ )
@@ -0,0 +1 @@
1
+ <span class="badge rounded-pill fw-normal border border-app-primary text-app-primary">{{ tag|title }} {{ weight|default:1 }}</span>
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  import string
5
+ from typing import Any
5
6
 
6
7
  from typing_extensions import Sequence, TYPE_CHECKING, TypeVar
7
8
 
@@ -48,6 +49,17 @@ def content_type_url(url_name: str, obj: T, **kwargs) -> str:
48
49
  return reverse(url_name, kwargs=kwargs)
49
50
 
50
51
 
52
+ @register.filter
53
+ def safe_dict_items(dictionary: dict) -> Any:
54
+ """
55
+ Explicitly call .items() on a dictionary, bypassing key lookup.
56
+ Use when the dict has a key named 'items' and you need the method.
57
+ """
58
+ if hasattr(dictionary, 'items'):
59
+ return dictionary.items()
60
+ return []
61
+
62
+
51
63
  @register.filter
52
64
  def in_list(value: str, arg: str) -> bool:
53
65
  """
@@ -5,7 +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
+ from django_spire.core.tag.mixins import TagModelMixin
9
9
  from django_spire.contrib.utils import truncate_string
10
10
  from django_spire.history.mixins import HistoryModelMixin
11
11
  from django_spire.knowledge.collection.querysets import CollectionQuerySet
@@ -16,7 +16,7 @@ from django_spire.knowledge.collection.services.service import CollectionGroupSe
16
16
  class Collection(
17
17
  HistoryModelMixin,
18
18
  OrderingModelMixin,
19
- TagsModelMixin,
19
+ TagModelMixin,
20
20
  ):
21
21
  parent = models.ForeignKey(
22
22
  'self',
@@ -4,14 +4,16 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from dandy import Prompt
6
6
 
7
- from django_spire.contrib.service import BaseDjangoModelService
8
- from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
7
+ from django_spire.core.tag.intelligence.tag_set_bot import TagSetBot
8
+ from django_spire.core.tag.service.tag_service import BaseTagService
9
+ from django_spire.core.tag.tools import simplify_tag_set, simplify_and_weight_tag_set_to_dict, \
10
+ get_score_percentage_from_tag_set_weighted
9
11
 
10
12
  if TYPE_CHECKING:
11
13
  from django_spire.knowledge.collection.models import Collection
12
14
 
13
15
 
14
- class CollectionTagService(BaseDjangoModelService['Collection']):
16
+ class CollectionTagService(BaseTagService['Collection']):
15
17
  obj: Collection
16
18
 
17
19
  def process_and_set_tags(self):
@@ -24,11 +26,11 @@ class CollectionTagService(BaseDjangoModelService['Collection']):
24
26
  content=collection_prompt
25
27
  )
26
28
 
27
- self.obj.set_tags_from_tag_set(
29
+ self.set_tags_from_tag_set(
28
30
  tag_set=tag_set,
29
31
  )
30
32
 
31
- def get_aggregated_tag_set(self):
33
+ def get_aggregated_tag_set(self) -> set[str]:
32
34
  tag_set = self.obj.tag_set
33
35
 
34
36
  for collection in self.obj.children.active():
@@ -39,14 +41,14 @@ class CollectionTagService(BaseDjangoModelService['Collection']):
39
41
 
40
42
  return tag_set
41
43
 
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)
44
+ def get_score_percentage_from_aggregated_tag_set_weighted(self, tag_set: set[str]) -> float:
45
+ return get_score_percentage_from_tag_set_weighted(
46
+ tag_set_actual=tag_set,
47
+ tag_set_reference=self.get_aggregated_tag_set()
48
+ )
50
49
 
51
- return tag_set
50
+ def get_simplified_aggregated_tag_set(self) -> set[str]:
51
+ return simplify_tag_set(self.get_aggregated_tag_set())
52
52
 
53
+ def get_simplified_and_weighted_aggregated_tag_set(self) -> dict[str, int]:
54
+ return simplify_and_weight_tag_set_to_dict(self.get_aggregated_tag_set())
@@ -3,7 +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
+ from django_spire.core.tag.mixins import TagModelMixin
7
7
  from django_spire.contrib.utils import truncate_string
8
8
  from django_spire.history.mixins import HistoryModelMixin
9
9
  from django_spire.knowledge.collection.models import Collection
@@ -15,7 +15,7 @@ from django_spire.knowledge.entry.version.models import EntryVersion
15
15
  class Entry(
16
16
  HistoryModelMixin,
17
17
  OrderingModelMixin,
18
- TagsModelMixin
18
+ TagModelMixin
19
19
  ):
20
20
  collection = models.ForeignKey(
21
21
  Collection,
@@ -30,3 +30,8 @@ class EntryQuerySet(HistoryQuerySet, OrderingQuerySetMixin):
30
30
  current_version__status=EntryVersionStatusChoices.DRAFT
31
31
  )
32
32
  )
33
+
34
+ def get_by_version_block_id(self, version_block_id: int) -> Entry:
35
+ return self.get(
36
+ current_version__block__id=version_block_id
37
+ )
@@ -4,21 +4,21 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from dandy import Prompt
6
6
 
7
- from django_spire.contrib.service import BaseDjangoModelService
8
- from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
7
+ from django_spire.core.tag.intelligence.tag_set_bot import TagSetBot
8
+ from django_spire.core.tag.service.tag_service import BaseTagService
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from django_spire.knowledge.entry.models import Entry
12
12
 
13
13
 
14
14
 
15
- class EntryTagService(BaseDjangoModelService['Entry']):
15
+ class EntryTagService(BaseTagService['Entry']):
16
16
  obj: Entry
17
17
 
18
18
  def process_and_set_tags(self):
19
19
  entry_prompt = Prompt()
20
20
 
21
- entry_prompt.text(self.obj.name)
21
+ entry_prompt.sub_heading(self.obj.name)
22
22
 
23
23
  for version_block in self.obj.current_version.blocks.all():
24
24
  entry_prompt.text(f'{version_block.render_to_text()}')
@@ -27,7 +27,7 @@ class EntryTagService(BaseDjangoModelService['Entry']):
27
27
  content=entry_prompt
28
28
  )
29
29
 
30
- self.obj.set_tags_from_tag_set(
30
+ self.set_tags_from_tag_set(
31
31
  tag_set=tag_set,
32
32
  )
33
33
 
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ from dandy import Bot, Prompt
5
+
6
+ from django_spire.knowledge.intelligence.intel.entry_intel import EntriesIntel
7
+
8
+ if TYPE_CHECKING:
9
+ from django_spire.knowledge.entry.models import Entry
10
+
11
+
12
+ class EntriesSearchBot(Bot):
13
+ llm_role = 'Knowledge Entry Search Assistant'
14
+ llm_task = 'Read through the knowledge and return information and the block id that relevant to the information request.'
15
+ llm_guidelines = (
16
+ Prompt()
17
+ .list([
18
+ 'Please read through all the blocks and return 2 of the most relevant ones.',
19
+ 'You can add any of the text in the knowledge entries to the 2 responses if it helps.',
20
+ 'Make sure the relevant heading text is from a heading with mark down formatting.',
21
+ 'When returning the relevant heading remove any of the markdown formating characters.',
22
+ ])
23
+ )
24
+ llm_intel_class = EntriesIntel
25
+
26
+ def process(self, user_input: str, entries: list[Entry]) -> EntriesIntel:
27
+
28
+ entry_prompt = Prompt()
29
+ entry_prompt.sub_heading('Information Request')
30
+ entry_prompt.line_break()
31
+ entry_prompt.text(f'{user_input}')
32
+ entry_prompt.line_break()
33
+ entry_prompt.sub_heading('Knowledge Entries')
34
+ entry_prompt.line_break()
35
+
36
+ for entry in entries:
37
+ for version_block in entry.current_version.blocks.all():
38
+ if version_block.render_to_text() != '\n':
39
+ entry_prompt.text(f'{version_block.id}: {version_block.render_to_text()}')
40
+
41
+ return self.llm.prompt_to_intel(
42
+ prompt=entry_prompt,
43
+ )
44
+
@@ -1,16 +1,36 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
4
+
3
5
  from dandy import BaseIntel, BaseListIntel
4
6
 
5
- from django_spire.knowledge.intelligence.intel.collection_intel import CollectionIntel
7
+ from django_spire.knowledge.entry.models import Entry
8
+
9
+
10
+ class EntryRelevancy(str, Enum):
11
+ EXTREMELY = 'Extremely'
12
+ VERY = 'Very'
13
+ SOMEWHAT = 'Somewhat'
14
+ NOT_SO_MUCH = 'Not so much'
15
+ NO_RELEVANCE = 'No Relevance'
6
16
 
7
17
 
8
18
  class EntryIntel(BaseIntel):
9
19
  relevant_heading_text: str
10
- relevant_subject_text: str
20
+ relevant_text: str
11
21
  relevant_block_id: int
12
- entry_id: int = None
13
- collection_intel: CollectionIntel = None
22
+ relevancy: EntryRelevancy
23
+
24
+ def __str__(self):
25
+ return self.relevant_heading_text + ' ' + self.relevant_text
26
+
27
+ @property
28
+ def collection(self):
29
+ return self.entry.collection
30
+
31
+ @property
32
+ def entry(self):
33
+ return Entry.objects.get_by_version_block_id(self.relevant_block_id)
14
34
 
15
35
 
16
36
  class EntriesIntel(BaseListIntel[EntryIntel]):
@@ -5,51 +5,72 @@ from typing import TYPE_CHECKING
5
5
  from django.core.handlers.wsgi import WSGIRequest
6
6
 
7
7
  from django_spire.ai.chat.message_intel import DefaultMessageIntel, BaseMessageIntel
8
- from django_spire.knowledge.intelligence.bots.entry_search_llm_bot import EntrySearchBot
9
- from django_spire.knowledge.intelligence.intel.entry_intel import EntriesIntel
8
+ from django_spire.core.tag.intelligence.tag_set_bot import TagSetBot
9
+ from django_spire.knowledge.collection.models import Collection
10
+ from django_spire.knowledge.intelligence.bots.entries_search_llm_bot import EntriesSearchBot
10
11
  from django_spire.knowledge.intelligence.intel.message_intel import KnowledgeMessageIntel
11
- from django_spire.knowledge.intelligence.decoders.collection_decoder import get_collection_decoder
12
- from django_spire.knowledge.intelligence.decoders.entry_decoder import get_entry_decoder
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from django.core.handlers.wsgi import WSGIRequest
16
15
  from dandy.llm.request.message import MessageHistory
17
16
 
18
-
19
17
  NO_KNOWLEDGE_MESSAGE_INTEL = DefaultMessageIntel(
20
18
  text='Sorry, I could not find any information on that.'
21
19
  )
22
20
 
23
21
 
24
22
  def knowledge_search_workflow(
25
- request: WSGIRequest,
26
- user_input: str,
27
- message_history: MessageHistory | None = None,
23
+ request: WSGIRequest,
24
+ user_input: str,
25
+ message_history: MessageHistory | None = None,
28
26
  ) -> BaseMessageIntel | None:
29
- collection_decoder = get_collection_decoder()
30
- collections = collection_decoder.process(user_input).values
27
+ user_tag_set = TagSetBot().process(user_input)
31
28
 
32
- if collections[0] is None:
33
- return NO_KNOWLEDGE_MESSAGE_INTEL
29
+ def get_top_scored_from_dict_to_list(
30
+ scored_dict: dict[str, float],
31
+ score_floor: float = 0.05
32
+ ) -> list:
33
+ if not scored_dict:
34
+ return []
34
35
 
35
- entries = []
36
+ min_score = min(scored_dict.values())
37
+ max_score = max(scored_dict.values())
36
38
 
37
- for collection in collections:
38
- if collection.entry_count > 0:
39
- entry_decoder = get_entry_decoder(collection=collection)
39
+ if min_score == 0 and max_score == 0:
40
+ return []
40
41
 
41
- entries.extend(entry_decoder.process(user_input).values)
42
+ median_score = (max_score - min_score) / 2
42
43
 
43
- entries = [entry for entry in entries if entry is not None]
44
+ top_scored_list = []
44
45
 
45
- if not entries:
46
- return NO_KNOWLEDGE_MESSAGE_INTEL
46
+ for key, value in scored_dict.items():
47
+ if value >= score_floor and value >= median_score:
48
+ top_scored_list.append(key)
47
49
 
48
- entries_intel = EntriesIntel(entry_intel_list=[])
50
+ return top_scored_list
49
51
 
50
- for entry in entries:
51
- entries_intel.append(
52
- EntrySearchBot().process(user_input=user_input, entry=entry)
52
+ collections = get_top_scored_from_dict_to_list({
53
+ collection: collection.services.tag.get_score_percentage_from_aggregated_tag_set_weighted(
54
+ user_tag_set
53
55
  )
56
+ for collection in Collection.objects.all().annotate_entry_count()
57
+ })
58
+
59
+ entries = get_top_scored_from_dict_to_list({
60
+ entry: entry.services.tag.get_score_percentage_from_tag_set_weighted(user_tag_set)
61
+ for collection in collections
62
+ for entry in collection.entries.all()
63
+ })
64
+
65
+ if not entries:
66
+ return NO_KNOWLEDGE_MESSAGE_INTEL
67
+
68
+ entries_intel = EntriesSearchBot(llm_temperature=0.0).process(
69
+ user_input=user_input,
70
+ entries=entries
71
+ )
54
72
 
55
- return KnowledgeMessageIntel(body=f'Entries: {entries_intel}', entries_intel=entries_intel)
73
+ return KnowledgeMessageIntel(
74
+ body=f'{" ".join([str(entry) for entry in entries_intel])}',
75
+ entries_intel=entries_intel,
76
+ )